forked from Yara724/api
resolved conflicts and maintained new features
This commit is contained in:
@@ -30,6 +30,7 @@ import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatu
|
||||
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 { buildResendItemsWithUi } from "src/Types&Enums/blame-request-management/resend-item-ui";
|
||||
import { buildFileLink } from "src/helpers/urlCreator";
|
||||
import { toJalaliDateAndTime } from "src/helpers/date-jalali";
|
||||
import { ResendRequestDto } from "./dto/resend.dto";
|
||||
@@ -37,6 +38,7 @@ 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";
|
||||
import { UserSignDbService } from "src/request-management/entities/db-service/sign.db.service";
|
||||
import { RequestManagementService } from "src/request-management/request-management.service";
|
||||
|
||||
interface CheckedRequestEntry {
|
||||
CheckedRequest?: {
|
||||
@@ -63,6 +65,10 @@ function statementToFormKey(
|
||||
@Injectable()
|
||||
export class ExpertBlameService {
|
||||
private readonly logger = new Logger(ExpertBlameService.name);
|
||||
|
||||
/** Blame V2 `workflow.locked` TTL (must match checks in lock/reply/resend). */
|
||||
private readonly blameV2LockTtlMs = 15 * 60 * 1000;
|
||||
|
||||
constructor(
|
||||
private readonly requestManagementDbService: RequestManagementDbService,
|
||||
private readonly blameRequestDbService: BlameRequestDbService,
|
||||
@@ -72,7 +78,8 @@ export class ExpertBlameService {
|
||||
private readonly expertDbService: ExpertDbService,
|
||||
private readonly blameDocumentDbService: BlameDocumentDbService,
|
||||
private readonly userSignDbService: UserSignDbService,
|
||||
) {}
|
||||
private readonly requestManagementService: RequestManagementService,
|
||||
) { }
|
||||
|
||||
async findAll(actor: any): Promise<AllRequestDtoRs> {
|
||||
// 1. Fetch all potentially relevant requests from the database.
|
||||
@@ -193,7 +200,13 @@ export class ExpertBlameService {
|
||||
requireActorClientKey(actor);
|
||||
const expertId = actor.sub;
|
||||
|
||||
const allCases = await this.blameRequestDbService.find({}, { lean: true });
|
||||
// Fetch all DISAGREEMENT cases
|
||||
const allCases = await this.blameRequestDbService.find(
|
||||
{
|
||||
blameStatus: BlameStatus.DISAGREEMENT,
|
||||
},
|
||||
{ lean: true },
|
||||
);
|
||||
|
||||
// Filter to show only:
|
||||
// 1. Same insurance tenant (party clientId or expert-initiated by this actor)
|
||||
@@ -239,6 +252,35 @@ export class ExpertBlameService {
|
||||
return false;
|
||||
});
|
||||
|
||||
const staleIds = new Set<string>();
|
||||
for (const doc of visibleCases) {
|
||||
const w = doc.workflow as Record<string, unknown> | undefined;
|
||||
if (!w?.locked) continue;
|
||||
const la = w.lockedAt;
|
||||
if (!la) {
|
||||
staleIds.add(String(doc._id));
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
Date.now() >=
|
||||
new Date(la as string | Date).getTime() + this.blameV2LockTtlMs
|
||||
) {
|
||||
staleIds.add(String(doc._id));
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
[...staleIds].map((id) => this.expireBlameCaseWorkflowLockV2IfStale(id)),
|
||||
);
|
||||
for (const doc of visibleCases) {
|
||||
if (!staleIds.has(String(doc._id))) continue;
|
||||
const w = doc.workflow as Record<string, unknown>;
|
||||
if (w) {
|
||||
w.locked = false;
|
||||
delete w.lockedAt;
|
||||
delete w.lockedBy;
|
||||
}
|
||||
}
|
||||
|
||||
const items: AllRequestDtoV2[] = visibleCases.map(
|
||||
(doc) => this.mapBlameRequestToListItemV2(doc),
|
||||
);
|
||||
@@ -294,6 +336,75 @@ export class ExpertBlameService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist unlock when blame V2 workflow lock is older than {@link blameV2LockTtlMs}.
|
||||
* V1 uses in-memory `scheduleUnlock`; V2 only stored `workflow.lockedAt`, so locks never cleared until now.
|
||||
*/
|
||||
private async expireBlameCaseWorkflowLockV2IfStale(
|
||||
requestId: string,
|
||||
): Promise<void> {
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
if (!request?.workflow?.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lockedAt = request.workflow.lockedAt;
|
||||
if (
|
||||
lockedAt &&
|
||||
Date.now() <
|
||||
new Date(lockedAt as Date).getTime() + this.blameV2LockTtlMs
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lockedById = request.workflow.lockedBy?.actorId;
|
||||
const filter: Record<string, unknown> = {
|
||||
_id: new Types.ObjectId(requestId),
|
||||
"workflow.locked": true,
|
||||
};
|
||||
if (lockedAt) {
|
||||
filter["workflow.lockedAt"] = lockedAt;
|
||||
} else if (lockedById) {
|
||||
filter["workflow.lockedBy.actorId"] = new Types.ObjectId(
|
||||
String(lockedById),
|
||||
);
|
||||
}
|
||||
|
||||
const cleared = await this.blameRequestDbService.findOneAndUpdate(
|
||||
filter as any,
|
||||
{
|
||||
$set: { "workflow.locked": false },
|
||||
$unset: { "workflow.lockedAt": "", "workflow.lockedBy": "" },
|
||||
},
|
||||
{ new: false },
|
||||
);
|
||||
|
||||
if (!cleared) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!request.expert?.decision && lockedById) {
|
||||
const rid = new Types.ObjectId(requestId).toString();
|
||||
try {
|
||||
await this.expertDbService.findOneAndUpdate(
|
||||
{ _id: new Types.ObjectId(String(lockedById)) },
|
||||
{
|
||||
$inc: { "requestStats.totalChecked": -1 },
|
||||
$pull: { countedRequests: rid } as any,
|
||||
},
|
||||
);
|
||||
this.logger.warn(
|
||||
`Blame case ${requestId} workflow lock expired; unlocked in DB and rolled back checked stat for expert ${lockedById}`,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
`Expert stat rollback failed after blame lock expiry ${requestId}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unlockApi(request, timer) {
|
||||
return setTimeout(async () => {
|
||||
try {
|
||||
@@ -410,9 +521,9 @@ export class ExpertBlameService {
|
||||
|
||||
// 2. Initial validation to ensure the expert has access
|
||||
// Check if locked by current expert and lock is still active
|
||||
const isLockedByCurrentExpert =
|
||||
const isLockedByCurrentExpert =
|
||||
String(request?.actorLocked?.actorId) === actorId && request.lockFile;
|
||||
|
||||
|
||||
// Check if lock has expired
|
||||
let isLockExpired = false;
|
||||
if (request.unlockTime) {
|
||||
@@ -420,7 +531,7 @@ export class ExpertBlameService {
|
||||
const now = Date.now();
|
||||
isLockExpired = now >= unlockTime;
|
||||
}
|
||||
|
||||
|
||||
if (isLockedByCurrentExpert && !isLockExpired) {
|
||||
// This is the correct expert, and the file is locked to them, which is fine.
|
||||
// They can access it even if they closed the browser and came back.
|
||||
@@ -431,7 +542,7 @@ export class ExpertBlameService {
|
||||
// The file is locked by someone else, or lock expired but status hasn't updated yet
|
||||
// Only block if lock is still active and not by current expert
|
||||
if (request.lockFile && !isLockExpired && !isLockedByCurrentExpert) {
|
||||
throw new BadRequestException("Request is locked by another expert");
|
||||
throw new BadRequestException("Request is locked by another expert");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,7 +652,7 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
// Access control: Expert-initiated files only visible to the initiating field expert
|
||||
// Access control
|
||||
const expertInitiated = doc.expertInitiated === true;
|
||||
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
||||
if (expertInitiated && initiatedByFieldExpertId && String(initiatedByFieldExpertId) !== actorId) {
|
||||
@@ -550,9 +661,6 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -561,61 +669,78 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
const parties = (doc.parties ?? []) as Array<{
|
||||
evidence?: { videoId?: string; voices?: string[] };
|
||||
[key: string]: unknown;
|
||||
const parties = Array.isArray(doc.parties) ? doc.parties : [];
|
||||
|
||||
const typedParties = parties as Array<{
|
||||
vehicle?: {
|
||||
inquiry?: {
|
||||
mapped?: any;
|
||||
};
|
||||
};
|
||||
evidence?: {
|
||||
videoId?: string | number;
|
||||
voices?: (string | number)[];
|
||||
videoUrl?: string;
|
||||
voiceUrls?: string[];
|
||||
};
|
||||
}>;
|
||||
for (const party of parties) {
|
||||
|
||||
// Evidence (videos + voices)
|
||||
for (const party of typedParties) {
|
||||
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);
|
||||
}
|
||||
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));
|
||||
}
|
||||
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();
|
||||
// Date formatting
|
||||
const createdAt = doc.createdAt ? new Date(doc.createdAt as string | number) : new Date();
|
||||
const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string | number) : 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 as any).inquiry;
|
||||
// Omit heavy SandHub inquiry blob from expert response (keep other vehicle fields)
|
||||
for (const party of typedParties) {
|
||||
const veh = party?.vehicle as Record<string, unknown> | undefined;
|
||||
if (veh && Object.prototype.hasOwnProperty.call(veh, "inquiry")) {
|
||||
delete veh.inquiry;
|
||||
}
|
||||
}
|
||||
|
||||
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 existing = await this.requestManagementDbService.findOne(requestId);
|
||||
if (!existing) {
|
||||
@@ -691,8 +816,8 @@ export class ExpertBlameService {
|
||||
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);
|
||||
|
||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||
// First, check the current state
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
if (!request) {
|
||||
@@ -726,7 +851,8 @@ export class ExpertBlameService {
|
||||
if (request.workflow?.locked) {
|
||||
const lockedAt = request.workflow.lockedAt;
|
||||
if (lockedAt) {
|
||||
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||
const lockExpiryTime =
|
||||
new Date(lockedAt).getTime() + this.blameV2LockTtlMs;
|
||||
if (Date.now() < lockExpiryTime) {
|
||||
// Lock is still valid
|
||||
const lockedByActorId = String(
|
||||
@@ -791,8 +917,19 @@ export class ExpertBlameService {
|
||||
requestId: string,
|
||||
resendDto: ResendRequestDto,
|
||||
actor: any,
|
||||
): Promise<{ requestId: string; status: string }> {
|
||||
): Promise<{
|
||||
requestId: string;
|
||||
status: string;
|
||||
parties: Array<{
|
||||
partyId: string;
|
||||
requestedItems: ResendItemType[];
|
||||
requestedItemsUi: ReturnType<typeof buildResendItemsWithUi>;
|
||||
description?: string;
|
||||
}>;
|
||||
}> {
|
||||
try {
|
||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
@@ -820,7 +957,8 @@ export class ExpertBlameService {
|
||||
// Validate lock hasn't expired
|
||||
const lockedAt = request.workflow?.lockedAt;
|
||||
if (lockedAt) {
|
||||
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||
const lockExpiryTime =
|
||||
new Date(lockedAt).getTime() + this.blameV2LockTtlMs;
|
||||
if (Date.now() > lockExpiryTime) {
|
||||
throw new ForbiddenException(
|
||||
"Your lock time has expired. Please lock the request again.",
|
||||
@@ -842,16 +980,42 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
const allowedItems = new Set<string>(Object.values(ResendItemType));
|
||||
const partyIdsOnFile = new Set(
|
||||
(request.parties || [])
|
||||
.map((p) => (p.person?.userId ? String(p.person.userId) : ""))
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
for (const party of resendDto.parties) {
|
||||
const uid = String(party.partyId);
|
||||
if (!partyIdsOnFile.has(uid)) {
|
||||
throw new BadRequestException(
|
||||
`partyId ${uid} is not a party userId on this blame request.`,
|
||||
);
|
||||
}
|
||||
for (const item of party.requestedItems || []) {
|
||||
if (!allowedItems.has(String(item))) {
|
||||
throw new BadRequestException(
|
||||
`Invalid requestedItems value: ${item}. Use ResendItemType values.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 partyResendRequests = resendDto.parties.map((party) => {
|
||||
const uid = String(party.partyId);
|
||||
return {
|
||||
partyId: new Types.ObjectId(uid),
|
||||
requestedItems: party.requestedItems,
|
||||
description: party.description || "",
|
||||
requestedAt: now,
|
||||
completed: false,
|
||||
uploadedDocuments: {},
|
||||
};
|
||||
});
|
||||
|
||||
const updatePayload = {
|
||||
$set: {
|
||||
@@ -865,9 +1029,6 @@ export class ExpertBlameService {
|
||||
},
|
||||
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||
},
|
||||
$push: {
|
||||
"workflow.completedSteps": "WAITING_FOR_GUILT_DECISION",
|
||||
},
|
||||
$unset: {
|
||||
"workflow.lockedAt": "",
|
||||
"workflow.lockedBy": "",
|
||||
@@ -885,11 +1046,21 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.requestManagementService.applyLinkedClaimsBlameResendStarted(
|
||||
requestId,
|
||||
);
|
||||
|
||||
// TODO: Send notifications to parties (SMS/Push) about required documents
|
||||
|
||||
return {
|
||||
requestId: String(request._id),
|
||||
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||
parties: partyResendRequests.map((p) => ({
|
||||
partyId: String(p.partyId),
|
||||
requestedItems: p.requestedItems as ResendItemType[],
|
||||
requestedItemsUi: buildResendItemsWithUi(p.requestedItems as string[]),
|
||||
description: p.description || undefined,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
@@ -907,6 +1078,12 @@ export class ExpertBlameService {
|
||||
/**
|
||||
* V2: Submit expert reply for blame case (blameCases collection).
|
||||
* Validates lock ownership and expiry, stores decision, unlocks, moves to WAITING_FOR_SIGNATURES.
|
||||
*
|
||||
* Note: V1 `replyRequest` also handled an “objection” path (`expertResendReply`) by writing
|
||||
* `expertSubmitReplyFinal` on the legacy request document. Blame V2 uses a single
|
||||
* `expert.decision` on `blameCases`; resend is modeled as `expert.resend` *before* the first
|
||||
* decision. If you need a second decision after signatures/objections, extend the schema
|
||||
* (e.g. decision revision) and branch here similarly to V1.
|
||||
*/
|
||||
async replyRequestV2(
|
||||
requestId: string,
|
||||
@@ -914,6 +1091,7 @@ export class ExpertBlameService {
|
||||
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);
|
||||
@@ -945,7 +1123,8 @@ export class ExpertBlameService {
|
||||
// Check if lock has expired (15 minutes)
|
||||
let isLockExpired = false;
|
||||
if (lockedAt) {
|
||||
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||
const lockExpiryTime =
|
||||
new Date(lockedAt).getTime() + this.blameV2LockTtlMs;
|
||||
isLockExpired = Date.now() > lockExpiryTime;
|
||||
}
|
||||
|
||||
@@ -1040,6 +1219,126 @@ export class ExpertBlameService {
|
||||
}
|
||||
}
|
||||
|
||||
async submitInPerson(
|
||||
requestId: string,
|
||||
actorId: string,
|
||||
) {
|
||||
try {
|
||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||
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() + this.blameV2LockTtlMs;
|
||||
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: null,
|
||||
description: "حضوری مراجعه شود.",
|
||||
decidedAt: now,
|
||||
decidedByExpertId: new Types.ObjectId(actorId),
|
||||
fields: {
|
||||
accidentWay: null,
|
||||
accidentReason: null,
|
||||
accidentType: null,
|
||||
},
|
||||
};
|
||||
|
||||
const updatePayload = {
|
||||
$set: {
|
||||
"workflow.locked": false,
|
||||
"workflow.currentStep": "COMPLETED",
|
||||
"workflow.nextStep": null,
|
||||
"expert.decision": decisionPayload,
|
||||
status: CaseStatus.STOPPED,
|
||||
},
|
||||
$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(
|
||||
"submitInPerson 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,
|
||||
@@ -1112,7 +1411,7 @@ export class ExpertBlameService {
|
||||
"Access denied to this request. You are not the locked expert.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Check if lock has expired (unlockTime has passed)
|
||||
if (request.unlockTime) {
|
||||
const unlockTime = new Date(request.unlockTime).getTime();
|
||||
@@ -1123,7 +1422,7 @@ export class ExpertBlameService {
|
||||
} else if (request.unlockTime == null) {
|
||||
throw new ForbiddenException("Your lock time has expired.");
|
||||
}
|
||||
|
||||
|
||||
if (!request.lockFile) {
|
||||
throw new ForbiddenException(
|
||||
"You must lock the request before submitting a reply.",
|
||||
@@ -1374,4 +1673,5 @@ export class ExpertBlameService {
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ResendRequestDto } from "./dto/resend.dto";
|
||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||
@Roles(RoleEnum.EXPERT, RoleEnum.FIELD_EXPERT)
|
||||
export class ExpertBlameV2Controller {
|
||||
constructor(private readonly expertBlameService: ExpertBlameService) {}
|
||||
constructor(private readonly expertBlameService: ExpertBlameService) { }
|
||||
|
||||
@Get()
|
||||
async findAll(@CurrentUser() actor: any) {
|
||||
@@ -101,4 +101,22 @@ export class ExpertBlameV2Controller {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Put("reply/inPerson/:id")
|
||||
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||
@ApiBody({ type: SubmitReplyDto })
|
||||
async submitInPerson(
|
||||
@Param("id") id: string,
|
||||
@Body() body: SubmitReplyDto,
|
||||
@CurrentUser() actor: any,
|
||||
) {
|
||||
try {
|
||||
return await this.expertBlameService.submitInPerson(id, actor.sub);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to submit expert reply for in person",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user