@@ -26,6 +26,9 @@ var _ component.Lifecycle = &Component{}
2626var rootDirectoryResourceTypes = []string {"Organization" , "Endpoint" }
2727var defaultDirectoryResourceTypes = []string {"Organization" , "Endpoint" , "Location" , "HealthcareService" , "PractitionerRole" , "Practitioner" }
2828
29+ // ParentOrganizationMap maps parent organizations (with URA identifier) to their linked child organizations
30+ type ParentOrganizationMap map [* fhir.Organization ][]* fhir.Organization
31+
2932// clockSkewBuffer is subtracted from local time when Bundle meta.lastUpdated is not available
3033// to account for potential clock differences between client and FHIR server
3134var clockSkewBuffer = 2 * time .Second
@@ -278,6 +281,13 @@ func (c *Component) updateFromDirectory(ctx context.Context, fhirBaseURLRaw stri
278281 c .processEndpointDeletes (ctx , deduplicatedEntries )
279282 }
280283
284+ // Find parent organizations with URA identifier and all organizations linked to them
285+ // This is used when validating organizations that don't have their own URA identifier
286+ parentOrganizationsMap , err := findParentOrganizationWithURA (ctx , deduplicatedEntries )
287+ if err != nil {
288+ return DirectoryUpdateReport {}, fmt .Errorf ("failed to find parent organization: %w" , err )
289+ }
290+
281291 // Build transaction with deterministic conditional references
282292 tx := fhir.Bundle {
283293 Type : fhir .BundleTypeTransaction ,
@@ -292,7 +302,7 @@ func (c *Component) updateFromDirectory(ctx context.Context, fhirBaseURLRaw stri
292302 continue
293303 }
294304 log .Ctx (ctx ).Trace ().Str ("fhir_server" , fhirBaseURLRaw ).Msgf ("Processing entry: %s" , entry .Request .Url )
295- resourceType , err := buildUpdateTransaction (ctx , & tx , entry , ValidationRules {AllowedResourceTypes : allowedResourceTypes }, allowDiscovery , fhirBaseURLRaw )
305+ resourceType , err := buildUpdateTransaction (ctx , & tx , entry , ValidationRules {AllowedResourceTypes : allowedResourceTypes }, parentOrganizationsMap , allowDiscovery , fhirBaseURLRaw )
296306 if err != nil {
297307 report .Warnings = append (report .Warnings , fmt .Sprintf ("entry #%d: %s" , i , err .Error ()))
298308 continue
@@ -473,3 +483,138 @@ func extractResourceIDFromURL(entry fhir.BundleEntry) string {
473483
474484 return ""
475485}
486+
487+ // buildOrganizationKey creates a composite key from organization ID and VersionId for deduplication.
488+ // The key includes both the ID and VersionId (if available) to differentiate between versions.
489+ func buildOrganizationKey (org * fhir.Organization ) string {
490+ if org == nil || org .Id == nil {
491+ return ""
492+ }
493+ key := * org .Id
494+ if org .Meta != nil && org .Meta .VersionId != nil {
495+ key = key + ":" + * org .Meta .VersionId
496+ }
497+ return key
498+ }
499+
500+ // findOrganizationKeyByID builds a map of organization keys by their ID to help with partOf reference lookups.
501+ // This is needed because partOf references typically only contain the ID, not the VersionId.
502+ func findOrganizationKeyByID (orgMap map [string ]* fhir.Organization , orgID string ) string {
503+ for key , org := range orgMap {
504+ if org .Id != nil && * org .Id == orgID {
505+ return key
506+ }
507+ }
508+ return ""
509+ }
510+
511+ // findParentOrganizationWithURA finds a parent organization with a URA identifier and all organizations linked to it.
512+ // It loops through ALL entries to find organizations with URA identifiers, then selects the one with the most
513+ // organizations linked to it (directly or indirectly through their partOf chain).
514+ // If no organization with URA is found directly, it traverses each organization's partOf chain to find a parent with URA.
515+ // Returns the parent organization with the most linked organizations and a slice of all organizations whose
516+ // partOf chain leads to the parent.
517+ // Returns (nil, nil, nil) if no organization with URA identifier is found (not an error condition).
518+ func findParentOrganizationWithURA (ctx context.Context , entries []fhir.BundleEntry ) (ParentOrganizationMap , error ) {
519+ result := make (ParentOrganizationMap )
520+
521+ // Build a map of all organizations for efficient lookup using ID and VersionId as composite key
522+ orgMap := make (map [string ]* fhir.Organization )
523+ for _ , entry := range entries {
524+ if entry .Resource == nil {
525+ continue
526+ }
527+ var org fhir.Organization
528+ if err := json .Unmarshal (entry .Resource , & org ); err != nil {
529+ continue
530+ }
531+ if org .Id != nil {
532+ key := buildOrganizationKey (& org )
533+ orgMap [key ] = & org
534+ }
535+ }
536+
537+ // Loop through all organizations to find all with URA identifier
538+ for _ , org := range orgMap {
539+ uraIdentifiers := libfhir .FilterIdentifiersBySystem (org .Identifier , coding .URANamingSystem )
540+ if len (uraIdentifiers ) > 0 {
541+ // Found an organization with URA, find all organizations linked to it
542+ linkedOrgs := findOrganizationsLinkedToParent (orgMap , org )
543+ result [org ] = linkedOrgs
544+ }
545+ }
546+
547+ return result , nil
548+ }
549+
550+ // findOrganizationsLinkedToParent returns all organizations whose partOf chain leads to the parent organization.
551+ // It excludes the parent organization itself from the returned slice.
552+ // Returns an empty slice (not nil) if no organizations are linked to the parent.
553+ func findOrganizationsLinkedToParent (orgMap map [string ]* fhir.Organization , parentOrg * fhir.Organization ) []* fhir.Organization {
554+ linked := make ([]* fhir.Organization , 0 )
555+
556+ for _ , org := range orgMap {
557+ // Skip the parent organization itself
558+ if org .Id != nil && parentOrg .Id != nil && * org .Id == * parentOrg .Id {
559+ continue
560+ }
561+
562+ // Check if this organization's partOf chain leads to the parent
563+ if organizationLinksToParent (orgMap , org , parentOrg ) {
564+ linked = append (linked , org )
565+ }
566+ }
567+
568+ return linked
569+ }
570+
571+ // organizationLinksToParent checks if an organization's partOf chain eventually leads to the parent organization.
572+ // It handles circular references by tracking visited organizations.
573+ func organizationLinksToParent (orgMap map [string ]* fhir.Organization , org * fhir.Organization , parentOrg * fhir.Organization ) bool {
574+ const maxDepth = 10
575+ visited := make (map [string ]bool )
576+ return organizationLinksToParentRecursive (orgMap , org , parentOrg , visited , 0 , maxDepth )
577+ }
578+
579+ // organizationLinksToParentRecursive is the recursive helper for organizationLinksToParent.
580+ func organizationLinksToParentRecursive (orgMap map [string ]* fhir.Organization , org * fhir.Organization , parentOrg * fhir.Organization , visited map [string ]bool , depth int , maxDepth int ) bool {
581+ if depth > maxDepth {
582+ return false // Depth exceeded
583+ }
584+
585+ if org .Id != nil {
586+ if visited [* org .Id ] {
587+ return false // Circular reference detected
588+ }
589+ visited [* org .Id ] = true
590+
591+ // Check if we found the parent
592+ if parentOrg .Id != nil && * org .Id == * parentOrg .Id {
593+ return true
594+ }
595+ }
596+
597+ // Check if this organization has a partOf reference
598+ if org .PartOf == nil || org .PartOf .Reference == nil {
599+ return false // No more parents in the chain
600+ }
601+
602+ // Extract the parent ID from the reference
603+ ref := * org .PartOf .Reference
604+ var parentID string
605+ if strings .Contains (ref , "/" ) {
606+ parts := strings .Split (ref , "/" )
607+ parentID = parts [len (parts )- 1 ]
608+ } else {
609+ parentID = ref
610+ }
611+
612+ // Look up the parent organization
613+ nextOrg , exists := orgMap [parentID ]
614+ if ! exists {
615+ return false // Parent not found in map
616+ }
617+
618+ // Recursively check the parent's chain
619+ return organizationLinksToParentRecursive (orgMap , nextOrg , parentOrg , visited , depth + 1 , maxDepth )
620+ }
0 commit comments