1
0
forked from Yara724/api

blame and claim refactored

This commit is contained in:
2026-02-24 12:19:25 +03:30
parent 0d8858f458
commit 35732dd70a
81 changed files with 11603 additions and 77 deletions

View File

@@ -101,3 +101,27 @@ export class AllRequestDtoRs {
this.data = requests.map((r) => new AllRequestDto(r));
}
}
/** V2: list item from blameCases (BlameRequest) for expert panel */
export class AllRequestDtoV2 {
requestId: string;
status: string;
userComment: null;
requestCode: string;
date: string;
time: string;
updatedAtDate: string;
updatedAtTime: string;
lockFile: boolean;
lockTime: string | null;
type: string;
blameStatus: string;
partiesInitialForms: { firstParty: string; secondParty: string };
}
export class AllRequestDtoRsV2 {
data: AllRequestDtoV2[];
constructor(items: AllRequestDtoV2[]) {
this.data = items;
}
}

View File

@@ -3,42 +3,81 @@ import { Types } from "mongoose";
import { BlameDocumentType } from "src/request-management/entities/schema/blame-document.schema";
export class AccidentWayIF {
@ApiProperty({ required: true })
@ApiProperty({ required: true, example: "9" })
id: string;
@ApiProperty({ required: true })
@ApiProperty({ required: true, example: "جلو به پهلو" })
label: string;
}
export class AccidentReasonIF {
@ApiProperty({ required: true })
@ApiProperty({ required: true, example: "23" })
id: string;
@ApiProperty({ required: true })
@ApiProperty({ required: true, example: "عدم رعايت فاصله جانبی" })
label: string;
@ApiProperty({ required: true })
@ApiProperty({ required: true, example: 3 })
fanavaran: number;
}
export class FieldsInterface {
@ApiProperty({ required: true })
@ApiProperty({
required: true,
example: { id: "9", label: "جلو به پهلو" },
})
accidentWay: AccidentWayIF;
@ApiProperty({ required: true })
@ApiProperty({
required: true,
example: {
id: "23",
label: "عدم رعايت فاصله جانبی",
fanavaran: 3,
},
})
accidentReason: AccidentReasonIF;
@ApiProperty({ required: true })
@ApiProperty({
required: true,
example: {
id: "3",
label: "برخورد یک وسیله نقلیه با وسیله نقلیه پارک شده",
},
})
accidentType: AccidentWayIF;
}
export class SubmitReplyDto {
@ApiProperty({ required: true })
@ApiProperty({
required: true,
example: "Expert's decision explanation",
description: "Detailed explanation of the expert's decision",
})
description: string;
@ApiProperty({ required: true, type: Types.ObjectId })
@ApiProperty({
required: true,
type: String,
example: "507f1f77bcf86cd799439011",
description: "ObjectId of the guilty party",
})
guiltyUserId: Types.ObjectId;
@ApiProperty({ required: true })
@ApiProperty({
required: true,
example: {
accidentWay: { id: "9", label: "جلو به پهلو" },
accidentReason: {
id: "23",
label: "عدم رعايت فاصله جانبی",
fanavaran: 3,
},
accidentType: {
id: "3",
label: "برخورد یک وسیله نقلیه با وسیله نقلیه پارک شده",
},
},
})
fields: FieldsInterface;
}

View File

@@ -0,0 +1,45 @@
import { ApiProperty } from "@nestjs/swagger";
import { Types } from "mongoose";
import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum";
export class PartyResendRequestDto {
@ApiProperty({
required: true,
type: String,
example: "507f1f77bcf86cd799439011",
description: "userId (person.userId) of the party member to request documents from",
})
partyId: Types.ObjectId;
@ApiProperty({
required: true,
isArray: true,
enum: ResendItemType,
example: [ResendItemType.DRIVING_LICENSE, ResendItemType.CAR_CERTIFICATE],
description: "Array of items to request from this party",
})
requestedItems: ResendItemType[];
@ApiProperty({
required: false,
example: "Please resend your driving license and car certificate with better quality",
description: "Optional explanation for why these items are needed",
})
description?: string;
}
export class ResendRequestDto {
@ApiProperty({
required: true,
type: [PartyResendRequestDto],
description: "Array of resend requests (can include first party, second party, or both)",
example: [
{
partyId: "507f1f77bcf86cd799439011",
requestedItems: ["drivingLicense", "carCertificate"],
description: "Please resend with better quality",
},
],
})
parties: PartyResendRequestDto[];
}

View File

@@ -3,12 +3,13 @@ import { ClientModule } from "src/client/client.module";
import { PlatesModule } from "src/plates/plates.module";
import { UsersModule } from "src/users/users.module";
import { ExpertBlameController } from "./expert-blame.controller";
import { ExpertBlameV2Controller } from "./expert-blame.v2.controller";
import { ExpertBlameService } from "./expert-blame.service";
import { RequestManagementModule } from "src/request-management/request-management.module";
@Module({
imports: [UsersModule, ClientModule, RequestManagementModule, PlatesModule],
controllers: [ExpertBlameController],
controllers: [ExpertBlameController, ExpertBlameV2Controller],
providers: [ExpertBlameService],
exports: [ExpertBlameService],
})

View File

@@ -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 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: 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,

View File

@@ -0,0 +1,104 @@
import {
Body,
Controller,
Get,
HttpException,
InternalServerErrorException,
Param,
Put,
UseGuards,
} from "@nestjs/common";
import { ApiBearerAuth, ApiBody, ApiParam, ApiTags } from "@nestjs/swagger";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertBlameService } from "./expert-blame.service";
import { SubmitReplyDto } from "./dto/reply.dto";
import { ResendRequestDto } from "./dto/resend.dto";
@ApiTags("expert-blame-panel (v2)")
@Controller("v2/expert-blame")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.EXPERT)
export class ExpertBlameV2Controller {
constructor(private readonly expertBlameService: ExpertBlameService) {}
@Get()
async findAll(@CurrentUser() actor: any) {
try {
return await this.expertBlameService.findAllV2(actor);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error ? error.message : "Failed to list blame cases",
);
}
}
@Get(":id")
@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);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error ? error.message : "Failed to get blame case details",
);
}
}
@Put("lock/:id")
@ApiParam({ name: "id", description: "Blame case request id" })
async lockRequest(@Param("id") id: string, @CurrentUser() actor: any) {
try {
return await this.expertBlameService.lockRequestV2(id, actor);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error ? error.message : "Failed to lock blame case",
);
}
}
@Put("reply/resend/:id")
@ApiParam({ name: "id", description: "Blame case request id" })
@ApiBody({ type: ResendRequestDto })
async resendRequest(
@Param("id") id: string,
@Body() body: ResendRequestDto,
@CurrentUser() actor: any,
) {
try {
return await this.expertBlameService.resendRequestV2(id, body, actor.sub);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error
? error.message
: "Failed to request document resend",
);
}
}
@Put("reply/submit/:id")
@ApiParam({ name: "id", description: "Blame case request id" })
@ApiBody({ type: SubmitReplyDto })
async submitReply(
@Param("id") id: string,
@Body() body: SubmitReplyDto,
@CurrentUser() actor: any,
) {
try {
return await this.expertBlameService.replyRequestV2(id, body, actor.sub);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error ? error.message : "Failed to submit expert reply",
);
}
}
}