import { useEffect, useState } from 'react'; import { AlertCircle, BadgeCheck, Loader2 } from 'lucide-react'; import { useAuthServiceContext } from '../../../services/AuthService.tsx'; import { useOrganizationServiceContext } from '../../../services/OrganizationService.tsx'; import { Button } from '../../../components/ui/button.tsx'; import { Input } from '../../../components/ui/input.tsx'; import { Label } from '../../../components/ui/label.tsx'; import { Alert, AlertDescription } from '../../../components/ui/alert.tsx'; import { relayServerFailure } from '../../../utils/fetchFailureMessage'; import { readOrganizationNpi } from '../../../utils/organizationNpi.ts'; import { useCapabilityServiceContext } from '../../../services/CapabilityService.tsx'; import { isSaveRetired, TIMED_OUT_MAY_HAVE_SAVED, useSaveScope } from '../../../utils/saveOperation'; import { holdSettingsWrite, nextSettingsAttempt, recordSettingsAttempt, settingsWriteHolder, useUnresolvedSettingsWrite, watchSettingsWrite } from '../../../utils/unresolvedSettingsWrite'; import { FRESH_REGISTRY_OUTAGE_NOTICE, HELD_REGISTRY_OUTAGE_NOTICE, isNpiRegistryUnavailable } from '../../../utils/npiRegistryUnavailable'; // The practice records or corrects its NPI. // // UNTIL THIS EXISTED THE NUMBER COULD ONLY BE SET ONCE, at signup, where it is optional. A practice // that skipped it had no way to record one afterwards and stayed invisible to the referral directory // forever, because the sender-side lookup matches on this identifier and nothing else ever wrote one. // // Mirrors AppointmentPolicySection's shape - seed from the organization on an id-keyed effect, save, // push the returned Organization back into auth. There is no GET: the NPI travels on the Organization // resource like every other org setting. const PracticeNpiSection = () => { const { currentOrganization, setWpOrganization } = useAuthServiceContext(); const { updateOrganizationNpi } = useOrganizationServiceContext(); const { mayCall } = useCapabilityServiceContext(); const [npi, setNpi] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(); const [saved, setSaved] = useState(false); // A save belongs to one practice, viewer (AuthService's key names the practice too) and right to save. When any of // them changes, or the section closes, a save started before sends nothing more and shows nothing. const organizationId = currentOrganization?.id; const owner = `${organizationId ?? ''}|${mayCall('PUT', '/organization-npi') === false ? 'refused' : 'may'}`; const scope = useSaveScope(owner, () => { setNpi(readOrganizationNpi(currentOrganization ?? undefined) ?? ''); setSaving(false); setError(undefined); setSaved(false); }); // A save sent and never answered: whether that NPI is recorded, and its one-practice claim confirmed, is not known // until the same NPI is sent again and answered. The practice's record cannot say so: it can carry the NPI while // the claim is still unconfirmed, and only the server settles that claim. const holder = settingsWriteHolder('practice-npi'); const unresolved = useUnresolvedSettingsWrite(holder); const stored = readOrganizationNpi(currentOrganization ?? undefined); // Keyed on the id rather than the object, the same guard the sibling sections carry: save calls // setWpOrganization with a new object reference, so keying on the object would re-seed immediately // after a save. useEffect(() => { setNpi(readOrganizationNpi(currentOrganization ?? undefined) ?? ''); setError(undefined); setSaved(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentOrganization?.id]); const handleSave = async (value = npi.trim()) => { const operation = scope.start(); const heldFor = holder; const startedWith = unresolved; const sending = nextSettingsAttempt(startedWith); // Only this route's registry-outage answer says the attempt wrote nothing; every other 5xx may have saved. let registryUnavailable = false; // Admitted again after the token and proof waits, and before its answer is used or installed. const write = watchSettingsWrite((onSend) => updateOrganizationNpi( value, { ...operation.transport, onSend }, { operationId: sending.id, attempt: sending.attempt }, ).catch((error: unknown) => { registryUnavailable = isNpiRegistryUnavailable(error); throw error; }), (status) => status < 500 || registryUnavailable); setSaving(true); setError(undefined); setSaved(false); try { const { organization } = await operation.step(write.run, true); holdSettingsWrite(heldFor, undefined); // A no-op save returns no organization - the server answers `unchanged` and writes nothing. // Pushing `undefined` into auth here would blank the whole org for every screen. if (organization) setWpOrganization(organization); // What was answered is what the field shows, even when a reopened page seeded it from an older record. setNpi(value); setSaved(true); } catch (err) { // Left with no answer, that NPI may be recorded, and the hold stays with whoever sent it even after they move // on. Answered without settling it (busy, say), the hold carries this attempt so the next goes past it. A // retired attempt never replaces a newer change or revives a settled one. recordSettingsAttempt(heldFor, sending, startedWith, write.unconfirmed() ? { kind: 'npi', state: 'unconfirmed', detail: value } : undefined); if (isSaveRetired(err) || !operation.belongs() || write.unconfirmed()) return; if (registryUnavailable) { setError(startedWith ? HELD_REGISTRY_OUTAGE_NOTICE : FRESH_REGISTRY_OUTAGE_NOTICE); return; } console.error('Error saving the practice NPI:', err); // THE SERVER'S OWN SENTENCE, and it is doing real work here rather than being relayed for // tidiness. There are three distinct refusals - malformed, not in the registry, already // registered to another practice - and each has a different next step for the person // reading it. A generic "could not save" sends them to the wrong one. setError(relayServerFailure(err, 'That NPI could not be saved. Please try again.')); } finally { if (operation.belongs()) setSaving(false); operation.retire(); } }; // Only the same NPI may be sent again, as the next attempt of the same change: the server runs it only after the // earlier save has finished and refuses that save if it arrives later, so it cannot overtake this one or a // correction made after it, and the resend's answer settles the claim. One save runs at a time here, and a save // from an earlier opening is retired before it can clear or replace a later hold. const heldNpi = unresolved?.detail; const held = unresolved?.state === 'unconfirmed'; return (

Practice NPI

Your organization’s NPI, as it appears in the national registry.

{ setNpi(event.target.value); setSaved(false); }} /> {/* SAYS WHAT RECORDING IT DOES, because a practice asked for a number with no stated purpose will leave the field empty - which is exactly the state that made this section necessary. */}

Other practices find you by this number when they send a referral. We check it against the national registry before saving it.

{held && ( {TIMED_OUT_MAY_HAVE_SAVED} {heldNpi && (
)}
)} {error && ( {error} )} {saved && !error && ( Saved and verified against the national registry. )}
); }; export default PracticeNpiSection;