import { BadRequestException, ForbiddenException } from "@nestjs/common"; import { RoleEnum } from "src/Types&Enums/role.enum"; /** 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); } /** True when `actor` is the field expert who created this expert-initiated blame file. */ export function blameCaseInitiatedByFieldExpert( doc: any, actor: { sub: string }, ): boolean { return ( !!doc?.expertInitiated && !!doc?.initiatedByFieldExpertId && String(doc.initiatedByFieldExpertId) === String(actor.sub) ); } 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.", ); } } /** * True when the field expert initiated this claim directly or via the linked * expert-initiated blame file (`blameRequestId` → blame.initiatedByFieldExpertId). */ export function claimCaseInitiatedByFieldExpert( doc: any, actor: { sub: string }, blame?: { expertInitiated?: boolean; initiatedByFieldExpertId?: unknown } | null, ): boolean { if ( doc?.initiatedByFieldExpertId && String(doc.initiatedByFieldExpertId) === String(actor.sub) ) { return true; } if ( blame?.expertInitiated && blame?.initiatedByFieldExpertId && String(blame.initiatedByFieldExpertId) === String(actor.sub) ) { return true; } return false; } /** * Authorization check that works for both damage experts (clientKey tenant * scope) and field experts (initiatedByFieldExpertId / linked blame ownership). */ export function assertClaimCaseForExpertActor( doc: any, actor: { sub: string; clientKey?: string }, blame?: { expertInitiated?: boolean; initiatedByFieldExpertId?: unknown } | null, ): void { if (claimCaseInitiatedByFieldExpert(doc, actor, blame)) return; assertClaimCaseForTenant(doc, actor); } 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; }