import type { ReferralAuthorizationRequest } from './referralAuthorization'; // --------------------------------------------------------------------------------------------- // S1a/S1b (client half) - inviting a practitioner who is not already on the team, and may // not be on Akashic at all. // // ONE entry field, two outcomes. The person inviting should not have to know whether the // practitioner already uses Akashic - they type an email or an NPI, and the system resolves it: // // found -> invite them by Practitioner/{id}, the ordinary member-invite path // not found-> an EXTERNAL invite: an emailed single-use token that lets them claim an account // // `POST /R4/collaboration-team-resolve-invitee` is deliberately an anti-oracle: every deny path // returns the SAME `{ found: false }` body and status, so the client must not try to distinguish // "no such practitioner" from "not allowed to look" - there is nothing there to distinguish. // --------------------------------------------------------------------------------------------- export interface ResolveInviteeQuery { email?: string; npi?: string; } export interface ExternalInviteRequest { teamId: string; email: string; npi: string; role?: string; authorization?: ReferralAuthorizationRequest; } const NPI_DIGITS = 10; const normalizeEmail = (value: string) => value.trim().toLowerCase(); /** Digits only - a human typing an NPI may include dashes or spaces from a directory listing. */ const normalizeNpi = (value: string) => value.replace(/\D/g, ''); /** * Decide whether what the user typed is an email or an NPI, and normalize it. * * Returns null when it is neither, rather than guessing: the server accepts EXACTLY one of * email|npi and 400s otherwise, so a guess turns a typo into an unexplained server error. The * caller shows a "enter an email address or a 10-digit NPI" message instead. */ export const buildResolveInviteeQuery = (typed: string): ResolveInviteeQuery | null => { const trimmed = typed.trim(); if (!trimmed) return null; const digits = normalizeNpi(trimmed); // Checked BEFORE the email branch: an NPI never contains '@', and a value that is all digits // (however punctuated) is unambiguously an NPI. if (digits.length === NPI_DIGITS && !trimmed.includes('@')) return { npi: digits }; // Deliberately minimal - the server validates properly. This only has to be good enough to // route the value to the right parameter. if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return { email: normalizeEmail(trimmed) }; return null; }; /** * The external-invite body. * * `authorization` is OMITTED entirely when absent rather than sent empty: a practitioner REFERRING * an outside colleague authorizes nothing - the patient does that at approval - and an empty block * would be a claim of an agreement that never happened. The server refuses a patient-initiated * invite without one, which is the case that must carry it. */ export const buildExternalInviteRequest = ( { teamId, email, npi, role, authorization }: { teamId: string; email: string; npi: string; role?: string; authorization?: ReferralAuthorizationRequest; }, ): ExternalInviteRequest => ({ teamId, email: normalizeEmail(email), npi: normalizeNpi(npi), ...(role ? { role } : {}), ...(authorization ? { authorization } : {}), });