import { BadRequestException, ForbiddenException } from "@nestjs/common"; /** Insurance company tenant id on the actor JWT (Mongo ObjectId string). */ export function requireActorClientKey(actor: { clientKey?: string }): string { const ck = actor?.clientKey; if (!ck || typeof ck !== "string") { throw new BadRequestException( "Client scope is missing for this account. Insurer/expert users must be bound to an insurance company.", ); } return ck; } export function blameCaseTouchesClient(doc: any, clientKey: string): boolean { const id = String(clientKey); // CAR_BODY: single party, always the first party's clientId if (doc?.type === "CAR_BODY") { const firstParty = (doc?.parties ?? []).find( (p: any) => p?.role === "FIRST", ); return String(firstParty?.person?.clientId ?? "") === id; } // THIRD_PARTY: only the guilty party's insurer sees this file const guiltyClientId = resolveGuiltyPartyClientId(doc); if (guiltyClientId) { return guiltyClientId === id; } // Fallback when guilt cannot be resolved yet — show to any party's insurer // This should be rare since guilt is always known upfront in your system return (doc?.parties ?? []).some( (p: any) => String(p?.person?.clientId ?? "") === id, ); } export function claimCaseTouchesClient(doc: any, clientKey: string): boolean { const id = String(clientKey); const owner = doc?.owner as | { clientId?: unknown; userClientKey?: unknown } | undefined; const tenantId = owner?.clientId ?? owner?.userClientKey; return String(tenantId ?? "") === id; } export function blameCaseAccessibleToExpert( doc: any, actor: { sub: string; clientKey?: string }, ): boolean { // Expert-initiated cases: only the initiating expert sees them if ( doc.expertInitiated && doc.initiatedByFieldExpertId && String(doc.initiatedByFieldExpertId) === actor.sub ) { return true; } const ck = actor?.clientKey; if (!ck) return false; // Uses the updated blameCaseTouchesClient — guilty party's clientId only return blameCaseTouchesClient(doc, ck); } export function assertBlameCaseForExpertTenant( doc: any, actor: { sub: string; clientKey?: string }, ): void { if (!blameCaseAccessibleToExpert(doc, actor)) { throw new ForbiddenException( "This blame case does not belong to your organization.", ); } } export function assertClaimCaseForTenant( doc: any, actor: { clientKey?: string }, ): void { const ck = requireActorClientKey(actor); if (!claimCaseTouchesClient(doc, ck)) { throw new ForbiddenException( "This claim does not belong to your organization.", ); } } export function resolveGuiltyPartyClientId(doc: any): string | null { const parties: any[] = doc?.parties ?? []; // Phase 1: expert decision exists — use it const guiltyUserId = doc?.expert?.decision?.guiltyPartyId ? String(doc.expert.decision.guiltyPartyId) : null; if (guiltyUserId) { const guiltyParty = parties.find( (p) => String(p?.person?.userId ?? "") === guiltyUserId, ); return String(guiltyParty?.person?.clientId ?? "") || null; } // Phase 2: no expert decision yet — use party self-statements // The party who admitsGuilt=true is the guilty one const guiltyByStatement = parties.find( (p) => p?.statement?.admitsGuilt === true, ); if (guiltyByStatement) { return String(guiltyByStatement?.person?.clientId ?? "") || null; } // Phase 3: fallback — use blameStatus field if set // AGREED means first party admitted guilt (convention in your system) if (doc?.blameStatus === "AGREED") { const firstParty = parties.find((p) => p?.role === "FIRST"); return String(firstParty?.person?.clientId ?? "") || null; } return null; }