1
0
forked from Yara724/api
Files
yara724-api/src/helpers/tenant-scope.ts
2026-06-08 10:22:02 +03:30

123 lines
3.7 KiB
TypeScript

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;
}