import { environment } from '../environments/environment'; import { dpopHeaderFor } from '../utils/dpopRequestHeader'; import type { AgreementBlock } from '../components/FirstSignInGate/agreementCopy'; import { akashicFetch } from '../config/clientManifest'; /** * WHY AN AGENCY STAFF MEMBER'S ACCOUNT DID NOT OPEN AT A PRACTICE, asked rather than guessed. * * The authorizer's refusal reaches the browser as a bare 403, so the cause has to come from reads that stay * open while it holds. The contractor Terms read is exempt from the Terms gate: it says whether Terms are * owed, and which version. When they are not owed, the practice's owner read is reachable, and its * operability says whether the practice itself is shut and why. */ export interface ContractorTermsDocument { version: string; title: string; blocks: AgreementBlock[]; } export type AgencyAccessCause = | { kind: 'terms-owed'; document: ContractorTermsDocument } | { kind: 'practice-shut'; message: string; refusal?: string } | { kind: 'not-terms' } | { kind: 'unreadable' }; type Read = { status: number; body: Record }; const call = async (method: 'GET' | 'POST', url: string, token: string, organizationId?: string, body?: unknown): Promise => { const response = await akashicFetch(url, { method, headers: { Authorization: `Bearer ${token}`, ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}), ...(organizationId ? { 'X-Organization-ID': organizationId } : {}), ...(await dpopHeaderFor(method, url)), }, ...(body !== undefined ? { body: JSON.stringify(body) } : {}), }); const parsed: unknown = await response.json().catch(() => undefined); return { status: response.status, body: parsed && typeof parsed === 'object' ? parsed as Record : {} }; }; const documentOf = (value: unknown): ContractorTermsDocument | undefined => { const doc = value as Partial | null | undefined; return doc && typeof doc.version === 'string' && typeof doc.title === 'string' && Array.isArray(doc.blocks) && doc.blocks.length > 0 ? (doc as ContractorTermsDocument) : undefined; }; export async function findAgencyAccessCause(token: string, organizationId: string | undefined): Promise { try { const terms = await call('GET', `${environment.orgAuthorizationApi}/contractor-terms`, token); if (terms.status !== 200) return { kind: 'unreadable' }; const document = documentOf(terms.body.document); if (terms.body.required === true && document) return { kind: 'terms-owed', document }; if (organizationId) { const owner = await call('GET', `${environment.apiUrl}/Organization/${organizationId}/owner`, token, organizationId).catch(() => undefined); const operability = owner?.status === 200 ? owner.body.operability as { operable?: unknown; refusal?: unknown; message?: unknown } | undefined : undefined; if (operability?.operable === false && typeof operability.message === 'string' && operability.message) { return { kind: 'practice-shut', message: operability.message, refusal: typeof operability.refusal === 'string' ? operability.refusal : undefined, }; } } return { kind: 'not-terms' }; } catch { return { kind: 'unreadable' }; } } /** Accepts the contractor Terms version this person was shown. */ export async function acceptContractorTermsVersion(token: string, version: string): Promise<'accepted' | 'changed' | 'failed'> { try { const answer = await call('POST', `${environment.orgAuthorizationApi}/contractor-terms/accept`, token, undefined, { documentVersion: version }); if (answer.status >= 200 && answer.status < 300) return 'accepted'; return answer.status === 409 ? 'changed' : 'failed'; } catch { return 'failed'; } }