forked from Yara724/api
Merge remote-tracking branch 'upstream/main' | Merge upstream into main and reapply local changes
This commit is contained in:
@@ -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 non–CAR_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(
|
||||
|
||||
@@ -42,7 +42,7 @@ export class ExpertBlameV2Controller {
|
||||
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||
async findOne(@Param("id") id: string, @CurrentUser() actor: any) {
|
||||
try {
|
||||
return await this.expertBlameService.findOneV2(id, actor.sub);
|
||||
return await this.expertBlameService.findOneV2(id, actor);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
@@ -73,7 +73,7 @@ export class ExpertBlameV2Controller {
|
||||
@CurrentUser() actor: any,
|
||||
) {
|
||||
try {
|
||||
return await this.expertBlameService.resendRequestV2(id, body, actor.sub);
|
||||
return await this.expertBlameService.resendRequestV2(id, body, actor);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
@@ -93,7 +93,7 @@ export class ExpertBlameV2Controller {
|
||||
@CurrentUser() actor: any,
|
||||
) {
|
||||
try {
|
||||
return await this.expertBlameService.replyRequestV2(id, body, actor.sub);
|
||||
return await this.expertBlameService.replyRequestV2(id, body, actor);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
|
||||
Reference in New Issue
Block a user