forked from Yara724/api
Resolve conflicts while keeping field-expert lock/view fixes and user party-access query improvements from the fork.
174 lines
5.2 KiB
TypeScript
174 lines
5.2 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 `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;
|
|
}
|