import { Address, ContactPoint, Extension, Organization } from 'fhir/r4'; const timezoneExtensionUrl = 'https://akashichealthrecords.com/fhir/extensions/tz-code'; const hasValue = (value: string | undefined): boolean => !!value && value.trim() !== ''; export const hasOrganizationMainContact = (organization: Organization | null | undefined): boolean => { return (organization?.telecom ?? []).some((contact: ContactPoint) => ['phone', 'email'].includes(contact.system ?? '') && hasValue(contact.value) ); }; export const hasOrganizationMainAddress = (organization: Organization | null | undefined): boolean => { const address: Address | undefined = organization?.address?.[0]; return !!address && hasValue(address.line?.[0]) && hasValue(address.city) && hasValue(address.state) && hasValue(address.postalCode); }; /** * - THIS IS NOT THE PRACTICE'S TIMEZONE CHECK, and reading it as one is how the schedule * fallback came to look safe. It answers only *"does the ORGANIZATION carry a zone"*. * * Spencer ruled on 2026-08-10 that **the Location is the authority** - *"the timezone belongs on the * location, single site orgs use the org"* - so a multi-state practice satisfies this with one zone while * every site but one is on the wrong clock. **The question booking asks is `resolveBookingTimeZone`** * (site, falling back to the org), and the onboarding step that surfaces it asks the same one, in * `orgSystemTasks.ts`'s `location` task. * * **It must NOT be added to `getOrganizationSetupMissingItems` below.** That gate gates five surfaces, * three of them read-only staff views, and several existing orgs have no timezone - see that function's * own warning about locking an org out of its own staff list. A guard in * `__tests__/aLocationWithoutAClockIsNotAFinishedLocation.test.ts` reds if anyone does. */ export const hasOrganizationTimezone = (organization: Organization | null | undefined): boolean => { return (organization?.extension ?? []).some((extension: Extension) => extension.url === timezoneExtensionUrl && hasValue(extension.valueCode) ); }; export const getOrganizationSetupMissingItems = (organization: Organization | null | undefined): string[] => { const missingItems: string[] = []; if (!hasValue(organization?.name)) { missingItems.push('organization name'); } if (!hasOrganizationMainContact(organization)) { missingItems.push('main phone or main email'); } if (!hasOrganizationMainAddress(organization)) { missingItems.push('primary organization location'); } return missingItems; }; export const isOrganizationSetupComplete = (organization: Organization | null | undefined): boolean => { return getOrganizationSetupMissingItems(organization).length === 0; }; /** * What is still missing before a NEW staff member may be created under this org. * * WHY THIS IS A SEPARATE FUNCTION AND NOT A WIDENING OF THE ONE ABOVE * * `isOrganizationSetupComplete` gates FIVE surfaces, and three of them are read-only staff VIEWS * (UserPage, UsersPage, UserEditPractitionerProfilePage). Adding a requirement there would lock an * existing org out of its own staff list to fix a creation-time problem - a much worse outcome than the * gap. This one is used only where a staff record is BROUGHT INTO EXISTENCE. * * THE TWO THINGS THE EXISTING GATE GETS WRONG, both found by tracing rather than by the card * * 1. **It says "primary organization location" and checks the org's ADDRESS.** `hasOrganizationMainAddress` * reads `Organization.address[0]` - a postal address on the Organization resource. A `Location` is a * separate FHIR resource with an id, and `PractitionerRole.location` references THAT. So an org can * have a complete address, pass this gate, and still have ZERO Locations - at which point every staff * member created gets an empty `PractitionerRole.location`, which the product reads as "covers every * site". **The message asserting the check it does not perform is very likely why the rule was * believed to be enforced** (Spencer, 2026-08-12: *"I'm pretty sure we force them to set that"*). * 2. **`hasOrganizationMainContact` accepts phone OR email**, so an org with only an email address * satisfies it. Spencer's *"same with primary phone number"* is a real additional requirement. * * Neither is repaired in place: `hasOrganizationMainAddress` and `hasOrganizationMainContact` are read * by those three view surfaces too, and tightening them there is a different decision with a lockout * risk. They are re-stated here, for the creation path, where the stricter reading is what was ruled. */ export const getStaffCreationBlockers = ( organization: Organization | null | undefined, siteCount: number, ): string[] => { const blockers = getOrganizationSetupMissingItems(organization); if (!hasOrganizationMainPhone(organization)) { // Named distinctly from the base gate's 'main phone or main email' so an admin holding only an // email address is told what is actually missing rather than seeing a requirement they believe // they already met. blockers.push('a main phone number'); } if (siteCount < 1) { blockers.push('at least one location'); } return blockers; }; export const hasOrganizationMainPhone = (organization: Organization | null | undefined): boolean => { return (organization?.telecom ?? []).some((contact: ContactPoint) => contact.system === 'phone' && hasValue(contact.value) ); }; export const canCreateStaff = ( organization: Organization | null | undefined, siteCount: number, ): boolean => getStaffCreationBlockers(organization, siteCount).length === 0;