1
0
forked from Yara724/api

Merge remote-tracking branch 'upstream/main' | Merge upstream into main and reapply local changes

This commit is contained in:
2026-04-18 12:30:32 +03:30
19 changed files with 1317 additions and 1323 deletions

View File

@@ -7,6 +7,11 @@ import {
Logger,
NotFoundException,
} from "@nestjs/common";
import {
assertBlameCaseForExpertTenant,
blameCaseAccessibleToExpert,
requireActorClientKey,
} from "src/helpers/tenant-scope";
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import {
@@ -192,6 +197,7 @@ export class ExpertBlameService {
*/
async findAllV2(actor: any): Promise<AllRequestDtoRsV2> {
try {
requireActorClientKey(actor);
const expertId = actor.sub;
// Fetch all DISAGREEMENT cases
@@ -203,10 +209,15 @@ export class ExpertBlameService {
);
// Filter to show only:
// 1. Fresh requests (WAITING_FOR_EXPERT and no decision)
// 2. Requests decided by current expert
// 3. Expert-initiated: only the initiating field expert sees them
// 1. Same insurance tenant (party clientId or expert-initiated by this actor)
// 2. Fresh requests (WAITING_FOR_EXPERT and no decision)
// 3. Requests decided by current expert
// 4. Expert-initiated: only the initiating field expert sees them
const visibleCases = (allCases as Record<string, unknown>[]).filter((doc) => {
if (!blameCaseAccessibleToExpert(doc, actor)) {
return false;
}
const expertInitiated = doc.expertInitiated === true;
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
@@ -625,100 +636,15 @@ export class ExpertBlameService {
* Excludes history. Returns only nonCAR_BODY types. Builds file links for all evidence.
* Access control: Only allows viewing fresh requests or requests decided by current expert.
*/
// async findOneV2(requestId: string, actorId: string): Promise<Record<string, unknown>> {
// try {
// const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
// if (!doc) {
// throw new NotFoundException("Request not found");
// }
// const type = doc.type as string;
// if (type === BlameRequestType.CAR_BODY) {
// throw new ForbiddenException(
// "CAR_BODY type requests are automatically handled and do not require expert review.",
// );
// }
// // Access control: Expert-initiated files only visible to the initiating field expert
// const expertInitiated = doc.expertInitiated === true;
// const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
// if (expertInitiated && initiatedByFieldExpertId && String(initiatedByFieldExpertId) !== actorId) {
// throw new ForbiddenException(
// "Only the field expert who created this file can view and review it.",
// );
// }
// // Allow if:
// // 1. No decision yet (fresh request)
// // 2. Decision made by current expert
// const decision = (doc.expert as any)?.decision;
// const decidedByExpertId = decision?.decidedByExpertId;
// if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
// throw new ForbiddenException(
// "You do not have permission to view this request. It has been handled by another expert.",
// );
// }
// const parties = (doc.parties ?? []) as Array<{
// evidence?: { videoId?: string; voices?: string[] };
// [key: string]: unknown;
// }>;
// for (const party of parties) {
// if (!party.evidence) continue;
// const evidence = party.evidence as Record<string, unknown>;
// if (evidence.videoId) {
// const videoDoc = await this.blameVideoDbService.findById(
// String(evidence.videoId),
// );
// if (videoDoc?.path) {
// evidence.videoUrl = buildFileLink(videoDoc.path);
// }
// }
// if (evidence.voices && Array.isArray(evidence.voices)) {
// const voiceUrls: string[] = [];
// for (const voiceId of evidence.voices) {
// const voiceDoc = await this.blameVoiceDbService.findById(
// String(voiceId),
// );
// if (voiceDoc?.path) {
// voiceUrls.push(buildFileLink(voiceDoc.path));
// }
// }
// evidence.voiceUrls = voiceUrls;
// }
// }
// const createdAt = doc.createdAt ? new Date(doc.createdAt as string | Date) : new Date();
// const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string | Date) : new Date();
// const [createdDate, createdTime] = toJalaliDateAndTime(createdAt);
// const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
// doc.createdAtFormatted = `${createdDate} ${createdTime}`;
// doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`;
// // Exclude mapped inquiry vehicle for both parties from response
// for (const party of parties) {
// delete party.vehicle;
// }
// return doc;
// } catch (error) {
// if (error instanceof HttpException) throw error;
// this.logger.error(
// "findOneV2 failed",
// requestId,
// error instanceof Error ? error.stack : String(error),
// );
// throw new InternalServerErrorException(
// error instanceof Error ? error.message : "Failed to get blame case details",
// );
// }
// }
async findOneV2(requestId: string, actorId: string): Promise<Record<string, unknown>> {
async findOneV2(requestId: string, actor: any): Promise<Record<string, unknown>> {
try {
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
requireActorClientKey(actor);
const actorId = actor.sub;
const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
if (!doc) throw new NotFoundException("Request not found");
if (!doc) {
throw new NotFoundException("Request not found");
}
assertBlameCaseForExpertTenant(doc, actor);
const type = doc.type as string;
if (type === BlameRequestType.CAR_BODY) {
throw new ForbiddenException(
@@ -821,12 +747,35 @@ export class ExpertBlameService {
async lockRequest(requestId: string, actorDetail) {
const existing = await this.requestManagementDbService.findOne(requestId);
if (!existing) {
throw new NotFoundException("Request not found");
}
if (existing.blameStatus === ReqBlameStatus.UserPending) {
throw new BadRequestException(
"Cannot lock request because blameStatus is UserPending",
);
}
const isLocked = !!existing.lockFile;
const lockedByCurrent =
String(existing?.actorLocked?.actorId || "") === String(actorDetail.sub);
const isLockExpired =
!!existing.unlockTime && Date.now() >= new Date(existing.unlockTime).getTime();
// Idempotent behavior: same expert can re-enter their locked file.
if (isLocked && lockedByCurrent && !isLockExpired) {
return { _id: requestId, lock: true, message: "Already locked by you" };
}
if (isLocked && !lockedByCurrent && !isLockExpired) {
throw new BadRequestException("Request is locked by another expert");
}
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
const updateResult = await this.requestManagementDbService.findOneAndUpdate(
{
_id: requestId,
lockFile: false,
blameStatus: { $ne: ReqBlameStatus.UserPending },
},
{
@@ -880,6 +829,8 @@ export class ExpertBlameService {
throw new NotFoundException("Request not found");
}
assertBlameCaseForExpertTenant(request, actorDetail);
// Validate request is available for expert review
if (
request.status !== CaseStatus.WAITING_FOR_EXPERT ||
@@ -909,7 +860,9 @@ export class ExpertBlameService {
new Date(lockedAt).getTime() + this.blameV2LockTtlMs;
if (Date.now() < lockExpiryTime) {
// Lock is still valid
const lockedByActorId = String(request.workflow.lockedBy?.actorId);
const lockedByActorId = String(
request.workflow.lockedBy?.actorId ?? "",
);
if (lockedByActorId === actorDetail.sub) {
throw new BadRequestException(
"You have already locked this request",
@@ -968,7 +921,7 @@ export class ExpertBlameService {
async resendRequestV2(
requestId: string,
resendDto: ResendRequestDto,
actorId: string,
actor: any,
): Promise<{
requestId: string;
status: string;
@@ -981,12 +934,17 @@ export class ExpertBlameService {
}> {
try {
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
requireActorClientKey(actor);
const actorId = actor.sub;
const request = await this.blameRequestDbService.findById(requestId);
if (!request) {
throw new NotFoundException("Request not found");
}
assertBlameCaseForExpertTenant(request, actor);
// Validate request is locked by current expert
if (!request.workflow?.locked) {
throw new ForbiddenException(
@@ -1135,16 +1093,20 @@ export class ExpertBlameService {
async replyRequestV2(
requestId: string,
reply: SubmitReplyDto,
actorId: string,
actor: any,
): Promise<{ requestId: string; status: string }> {
try {
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
requireActorClientKey(actor);
const actorId = actor.sub;
const request = await this.blameRequestDbService.findById(requestId);
if (!request) {
throw new NotFoundException("Request not found");
}
assertBlameCaseForExpertTenant(request, actor);
// Validate no decision exists yet
if (request.expert?.decision) {
throw new ForbiddenException(