forked from Yara724/api
blame and claim refactored
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
HttpException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||||
import { AllRequestDtoRs } from "./dto/all-request.dto";
|
||||
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||
import {
|
||||
AllRequestDtoRs,
|
||||
AllRequestDtoV2,
|
||||
AllRequestDtoRsV2,
|
||||
} from "./dto/all-request.dto";
|
||||
import { UserType } from "src/Types&Enums/userType.enum";
|
||||
import { SubmitReplyDto } from "./dto/reply.dto";
|
||||
import { Types } from "mongoose";
|
||||
@@ -14,7 +21,13 @@ import { BlameVideoDbService } from "src/request-management/entities/db-service/
|
||||
import { BlameVoiceDbService } from "src/request-management/entities/db-service/blame.voice.db.service";
|
||||
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
|
||||
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
|
||||
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||
import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum";
|
||||
import { buildFileLink } from "src/helpers/urlCreator";
|
||||
import { toJalaliDateAndTime } from "src/helpers/date-jalali";
|
||||
import { ResendRequestDto } from "./dto/resend.dto";
|
||||
import { readFile } from "fs/promises";
|
||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||
import { BlameDocumentDbService } from "src/request-management/entities/db-service/blame-document.db.service";
|
||||
@@ -28,11 +41,26 @@ interface CheckedRequestEntry {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function statementToFormKey(
|
||||
statement?: {
|
||||
admitsGuilt?: boolean;
|
||||
claimsDamage?: boolean;
|
||||
acceptsExpertOpinion?: boolean;
|
||||
},
|
||||
): string | null {
|
||||
if (!statement) return null;
|
||||
if (statement.admitsGuilt) return "imGuilty";
|
||||
if (statement.claimsDamage) return "imDamaged";
|
||||
if (statement.acceptsExpertOpinion) return "expertOpinion";
|
||||
return null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ExpertBlameService {
|
||||
private readonly logger = new Logger(ExpertBlameService.name);
|
||||
constructor(
|
||||
private readonly requestManagementDbService: RequestManagementDbService,
|
||||
private readonly blameRequestDbService: BlameRequestDbService,
|
||||
private readonly clientDbService: ClientDbService,
|
||||
private readonly blameVideoDbService: BlameVideoDbService,
|
||||
private readonly blameVoiceDbService: BlameVoiceDbService,
|
||||
@@ -148,6 +176,108 @@ export class ExpertBlameService {
|
||||
return new AllRequestDtoRs(visibleRequests);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: List blame cases for current expert.
|
||||
* Shows:
|
||||
* 1. Fresh requests (WAITING_FOR_EXPERT with no decision)
|
||||
* 2. Requests where current expert made the decision
|
||||
* Does NOT show requests decided by other experts.
|
||||
*/
|
||||
async findAllV2(actor: any): Promise<AllRequestDtoRsV2> {
|
||||
try {
|
||||
const expertId = actor.sub;
|
||||
|
||||
// Fetch all DISAGREEMENT cases
|
||||
const allCases = await this.blameRequestDbService.find(
|
||||
{
|
||||
blameStatus: BlameStatus.DISAGREEMENT,
|
||||
},
|
||||
{ lean: true },
|
||||
);
|
||||
|
||||
// Filter to show only:
|
||||
// 1. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
||||
// 2. Requests decided by current expert
|
||||
const visibleCases = (allCases as Record<string, unknown>[]).filter((doc) => {
|
||||
const status = doc.status as string;
|
||||
const decision = doc.expert as any;
|
||||
const decidedByExpertId = decision?.decision?.decidedByExpertId;
|
||||
const hasDecision = !!decision?.decision;
|
||||
|
||||
// Fresh request (no decision yet)
|
||||
if (status === CaseStatus.WAITING_FOR_EXPERT && !hasDecision) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Request decided by current expert
|
||||
if (decidedByExpertId && String(decidedByExpertId) === expertId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Locked by current expert but no decision yet
|
||||
const lockedBy = decision?.resend?.requestedByExpertId || (doc.workflow as any)?.lockedBy?.actorId;
|
||||
if (status === CaseStatus.WAITING_FOR_EXPERT && lockedBy && String(lockedBy) === expertId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const items: AllRequestDtoV2[] = visibleCases.map(
|
||||
(doc) => this.mapBlameRequestToListItemV2(doc),
|
||||
);
|
||||
return new AllRequestDtoRsV2(items);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error("findAllV2 failed", error instanceof Error ? error.stack : String(error));
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to list blame cases",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private mapBlameRequestToListItemV2(
|
||||
doc: Record<string, unknown>,
|
||||
): AllRequestDtoV2 {
|
||||
const createdAt = doc.createdAt ? new Date(doc.createdAt as string) : new Date();
|
||||
const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string) : new Date();
|
||||
const [date, time] = toJalaliDateAndTime(createdAt);
|
||||
const [updatedAtDate, updatedAtTime] = toJalaliDateAndTime(updatedAt);
|
||||
const workflow = (doc.workflow ?? {}) as Record<string, unknown>;
|
||||
const parties = (doc.parties ?? []) as Array<{
|
||||
role?: string;
|
||||
statement?: {
|
||||
admitsGuilt?: boolean;
|
||||
claimsDamage?: boolean;
|
||||
acceptsExpertOpinion?: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
const firstParty = parties.find((p) => p.role === "FIRST");
|
||||
const secondParty = parties.find((p) => p.role === "SECOND");
|
||||
|
||||
return {
|
||||
requestId: String(doc._id),
|
||||
status: String(doc.status ?? ""),
|
||||
userComment: null,
|
||||
requestCode: String(doc.publicId ?? ""),
|
||||
date,
|
||||
time,
|
||||
updatedAtDate,
|
||||
updatedAtTime,
|
||||
lockFile: Boolean(workflow.locked),
|
||||
lockTime: workflow.lockedAt
|
||||
? new Date(workflow.lockedAt as string).toISOString()
|
||||
: null,
|
||||
type: String(doc.type ?? "THIRD_PARTY"),
|
||||
blameStatus: String(doc.blameStatus ?? ""),
|
||||
partiesInitialForms: {
|
||||
firstParty: statementToFormKey(firstParty?.statement) ?? "",
|
||||
secondParty: statementToFormKey(secondParty?.statement) ?? "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public unlockApi(request, timer) {
|
||||
return setTimeout(async () => {
|
||||
try {
|
||||
@@ -374,6 +504,92 @@ export class ExpertBlameService {
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Get blame case details by id from blameCases collection.
|
||||
* 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: Check if expert has permission to view this request
|
||||
const decision = (doc.expert as any)?.decision;
|
||||
const decidedByExpertId = decision?.decidedByExpertId;
|
||||
|
||||
// Allow if:
|
||||
// 1. No decision yet (fresh request)
|
||||
// 2. Decision made by current expert
|
||||
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 lockRequest(requestId: string, actorDetail) {
|
||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||
|
||||
@@ -418,6 +634,340 @@ export class ExpertBlameService {
|
||||
return { _id: requestId, lock: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Lock blame case for 15 minutes (blameCases collection).
|
||||
* Sets workflow.locked, workflow.lockedBy, workflow.lockedAt.
|
||||
* Checks if existing lock is expired and allows re-locking.
|
||||
*/
|
||||
async lockRequestV2(requestId: string, actorDetail: any): Promise<{ _id: string; lock: boolean }> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const fifteenMinutesAgo = new Date(now.getTime() - 15 * 60 * 1000);
|
||||
|
||||
// First, check the current state
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
if (!request) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
|
||||
// Validate request is available for expert review
|
||||
if (
|
||||
request.status !== CaseStatus.WAITING_FOR_EXPERT ||
|
||||
request.blameStatus !== BlameStatus.DISAGREEMENT
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Request is not available for expert review",
|
||||
);
|
||||
}
|
||||
|
||||
// Check if locked and not expired
|
||||
if (request.workflow?.locked) {
|
||||
const lockedAt = request.workflow.lockedAt;
|
||||
if (lockedAt) {
|
||||
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||
if (Date.now() < lockExpiryTime) {
|
||||
// Lock is still valid
|
||||
const lockedByActorId = String(request.workflow.lockedBy?.actorId);
|
||||
if (lockedByActorId === actorDetail.sub) {
|
||||
throw new BadRequestException(
|
||||
"You have already locked this request",
|
||||
);
|
||||
} else {
|
||||
throw new BadRequestException(
|
||||
"Request is currently locked by another expert",
|
||||
);
|
||||
}
|
||||
}
|
||||
// Lock expired, allow re-locking (continue below)
|
||||
}
|
||||
}
|
||||
|
||||
// Lock the request (either unlocked or expired lock)
|
||||
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||
requestId,
|
||||
{
|
||||
$set: {
|
||||
"workflow.locked": true,
|
||||
"workflow.lockedAt": now,
|
||||
"workflow.lockedBy": {
|
||||
actorId: new Types.ObjectId(actorDetail.sub),
|
||||
actorName: actorDetail.fullName || "Unknown Expert",
|
||||
actorRole: "expert",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!updateResult) {
|
||||
throw new InternalServerErrorException("Failed to lock the request");
|
||||
}
|
||||
|
||||
// Update expert stats (reusing existing helper)
|
||||
await this.updateDamageExpertStats(actorDetail.sub, requestId, "checked");
|
||||
|
||||
return { _id: requestId, lock: true };
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error(
|
||||
"lockRequestV2 failed",
|
||||
requestId,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to lock blame case",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Request parties to resend documents/evidence (blameCases collection).
|
||||
* Expert can request one or both parties to provide additional or better quality evidence.
|
||||
*/
|
||||
async resendRequestV2(
|
||||
requestId: string,
|
||||
resendDto: ResendRequestDto,
|
||||
actorId: string,
|
||||
): Promise<{ requestId: string; status: string }> {
|
||||
try {
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
|
||||
if (!request) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
|
||||
// Validate request is locked by current expert
|
||||
if (!request.workflow?.locked) {
|
||||
throw new ForbiddenException(
|
||||
"You must lock the request before requesting document resend.",
|
||||
);
|
||||
}
|
||||
|
||||
const lockedByActorId = String(request.workflow?.lockedBy?.actorId || "");
|
||||
if (lockedByActorId !== actorId) {
|
||||
throw new ForbiddenException(
|
||||
"Access denied. You are not the locked expert for this request.",
|
||||
);
|
||||
}
|
||||
|
||||
// Validate lock hasn't expired
|
||||
const lockedAt = request.workflow?.lockedAt;
|
||||
if (lockedAt) {
|
||||
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||
if (Date.now() > lockExpiryTime) {
|
||||
throw new ForbiddenException(
|
||||
"Your lock time has expired. Please lock the request again.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate at least one party is specified
|
||||
if (!resendDto.parties || resendDto.parties.length === 0) {
|
||||
throw new BadRequestException(
|
||||
"At least one party must be specified for resend request",
|
||||
);
|
||||
}
|
||||
|
||||
// Validate no existing expert decision
|
||||
if (request.expert?.decision) {
|
||||
throw new ForbiddenException(
|
||||
"Cannot request resend after expert decision has been made.",
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Build resend requests for parties
|
||||
const partyResendRequests = resendDto.parties.map((party) => ({
|
||||
partyId: new Types.ObjectId(String(party.partyId)),
|
||||
requestedItems: party.requestedItems,
|
||||
description: party.description || "",
|
||||
requestedAt: now,
|
||||
completed: false,
|
||||
}));
|
||||
|
||||
const updatePayload = {
|
||||
$set: {
|
||||
"workflow.locked": false,
|
||||
"workflow.currentStep": "WAITING_FOR_DOCUMENT_RESEND",
|
||||
"workflow.nextStep": "WAITING_FOR_GUILT_DECISION",
|
||||
"expert.resend": {
|
||||
parties: partyResendRequests,
|
||||
requestedAt: now,
|
||||
requestedByExpertId: new Types.ObjectId(actorId),
|
||||
},
|
||||
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||
},
|
||||
$push: {
|
||||
"workflow.completedSteps": "WAITING_FOR_GUILT_DECISION",
|
||||
},
|
||||
$unset: {
|
||||
"workflow.lockedAt": "",
|
||||
"workflow.lockedBy": "",
|
||||
},
|
||||
};
|
||||
|
||||
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||
requestId,
|
||||
updatePayload,
|
||||
);
|
||||
|
||||
if (!updateResult) {
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to update request with resend request",
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Send notifications to parties (SMS/Push) about required documents
|
||||
|
||||
return {
|
||||
requestId: String(request._id),
|
||||
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error(
|
||||
"resendRequestV2 failed",
|
||||
requestId,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to request document resend",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Submit expert reply for blame case (blameCases collection).
|
||||
* Validates lock ownership and expiry, stores decision, unlocks, moves to WAITING_FOR_SIGNATURES.
|
||||
*/
|
||||
async replyRequestV2(
|
||||
requestId: string,
|
||||
reply: SubmitReplyDto,
|
||||
actorId: string,
|
||||
): Promise<{ requestId: string; status: string }> {
|
||||
try {
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
|
||||
if (!request) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
|
||||
// Validate no decision exists yet
|
||||
if (request.expert?.decision) {
|
||||
throw new ForbiddenException(
|
||||
"This request already has an expert decision.",
|
||||
);
|
||||
}
|
||||
|
||||
// Check if request is locked
|
||||
if (!request.workflow?.locked) {
|
||||
throw new ForbiddenException(
|
||||
"You must lock the request before submitting a reply.",
|
||||
);
|
||||
}
|
||||
|
||||
const lockedByActorId = String(request.workflow?.lockedBy?.actorId || "");
|
||||
const lockedAt = request.workflow?.lockedAt;
|
||||
const isLockedByCurrentActor = lockedByActorId === actorId;
|
||||
|
||||
// Check if lock has expired (15 minutes)
|
||||
let isLockExpired = false;
|
||||
if (lockedAt) {
|
||||
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||
isLockExpired = Date.now() > lockExpiryTime;
|
||||
}
|
||||
|
||||
// Handle different lock scenarios
|
||||
if (!isLockedByCurrentActor) {
|
||||
// Request is locked by another expert
|
||||
if (isLockExpired) {
|
||||
throw new ForbiddenException(
|
||||
"You must lock the request first before submitting a reply.",
|
||||
);
|
||||
} else {
|
||||
throw new ForbiddenException(
|
||||
"This request is currently locked by another expert.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Current actor is the lock owner
|
||||
if (isLockExpired) {
|
||||
throw new ForbiddenException(
|
||||
"Your lock time has expired. Please lock the request again before submitting.",
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Build expert decision with fields
|
||||
const decisionPayload: any = {
|
||||
guiltyPartyId: new Types.ObjectId(String(reply.guiltyUserId)),
|
||||
description: reply.description,
|
||||
decidedAt: now,
|
||||
decidedByExpertId: new Types.ObjectId(actorId),
|
||||
fields: {
|
||||
accidentWay: {
|
||||
id: reply.fields.accidentWay.id,
|
||||
label: reply.fields.accidentWay.label,
|
||||
},
|
||||
accidentReason: {
|
||||
id: reply.fields.accidentReason.id,
|
||||
label: reply.fields.accidentReason.label,
|
||||
fanavaran: reply.fields.accidentReason.fanavaran,
|
||||
},
|
||||
accidentType: {
|
||||
id: reply.fields.accidentType.id,
|
||||
label: reply.fields.accidentType.label,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const updatePayload = {
|
||||
$set: {
|
||||
"workflow.locked": false,
|
||||
"workflow.currentStep": "WAITING_FOR_SIGNATURES",
|
||||
"workflow.nextStep": null,
|
||||
"expert.decision": decisionPayload,
|
||||
status: CaseStatus.WAITING_FOR_SIGNATURES,
|
||||
},
|
||||
$push: {
|
||||
"workflow.completedSteps": "WAITING_FOR_GUILT_DECISION",
|
||||
},
|
||||
$unset: {
|
||||
"workflow.lockedAt": "",
|
||||
"workflow.lockedBy": "",
|
||||
},
|
||||
};
|
||||
|
||||
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||
requestId,
|
||||
updatePayload,
|
||||
);
|
||||
|
||||
if (!updateResult) {
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to update request with expert reply",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
requestId: String(request._id),
|
||||
status: CaseStatus.WAITING_FOR_SIGNATURES,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error(
|
||||
"replyRequestV2 failed",
|
||||
requestId,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to submit expert reply",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateDamageExpertStats(
|
||||
expertId: string,
|
||||
requestId: string,
|
||||
|
||||
Reference in New Issue
Block a user