1
0
forked from Yara724/api
Files
yara724-api/src/helpers/tenant-scope.ts

163 lines
4.9 KiB
TypeScript

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 the acting field expert created this expert-initiated blame file. */
export function blameCaseInitiatedByFieldExpert(
doc: any,
actor: { sub: string; role?: string },
): boolean {
return (
actor.role === RoleEnum.FIELD_EXPERT &&
!!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.",
);
}
}
/**
* Returns true when a field expert (no clientKey) initiated this claim via an
* expert-initiated IN_PERSON blame file.
*/
export function claimCaseInitiatedByFieldExpert(
doc: any,
actor: { sub: string },
): boolean {
return (
!!doc.initiatedByFieldExpertId &&
String(doc.initiatedByFieldExpertId) === String(actor.sub)
);
}
/**
* Authorization check that works for both damage experts (clientKey tenant
* scope) and field experts (initiatedByFieldExpertId ownership).
*/
export function assertClaimCaseForExpertActor(
doc: any,
actor: { sub: string; clientKey?: string },
): void {
if (claimCaseInitiatedByFieldExpert(doc, actor)) 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;
}