1
0
forked from Yara724/api

vehicle data added to get single request by expert , also submit in person

This commit is contained in:
Soheil Hajizadeh
2026-04-08 16:08:31 +03:30
parent b216a363fb
commit 1d51370b3e
3 changed files with 298 additions and 54 deletions

View File

@@ -1,19 +1,21 @@
//! NEW
export enum CaseStatus {
OPEN = "OPEN",
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
COMPLETED = "COMPLETED",
CANCELLED = "CANCELLED",
AUTO_CLOSED = "AUTO_CLOSED"
}
OPEN = "OPEN",
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
COMPLETED = "COMPLETED",
CANCELLED = "CANCELLED",
AUTO_CLOSED = "AUTO_CLOSED",
STOPPED = "STOPPED"
}

View File

@@ -67,7 +67,7 @@ export class ExpertBlameService {
private readonly expertDbService: ExpertDbService,
private readonly blameDocumentDbService: BlameDocumentDbService,
private readonly userSignDbService: UserSignDbService,
) {}
) { }
async findAll(actor: any): Promise<AllRequestDtoRs> {
// 1. Fetch all potentially relevant requests from the database.
@@ -186,7 +186,7 @@ export class ExpertBlameService {
async findAllV2(actor: any): Promise<AllRequestDtoRsV2> {
try {
const expertId = actor.sub;
// Fetch all DISAGREEMENT cases
const allCases = await this.blameRequestDbService.find(
{
@@ -405,9 +405,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) {
@@ -415,7 +415,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.
@@ -426,7 +426,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");
}
}
@@ -520,12 +520,99 @@ 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>> {
try {
const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
if (!doc) {
throw new NotFoundException("Request not found");
}
if (!doc) throw new NotFoundException("Request not found");
const type = doc.type as string;
if (type === BlameRequestType.CAR_BODY) {
throw new ForbiddenException(
@@ -533,7 +620,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) {
@@ -542,9 +629,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) {
@@ -553,61 +637,83 @@ export class ExpertBlameService {
);
}
const parties = (doc.parties ?? []) as Array<{
evidence?: { videoId?: string; voices?: string[] };
[key: string]: unknown;
const parties = (doc.parties ?? []);
const typedParties = parties as Array<{
vehicle?: {
inquiry?: {
mapped?: any; // Use 'any' or a more specific type if known
// other inquiry properties
};
// other vehicle properties
};
evidence?: { // For the second part of your original error description
videoId?: string | number;
voices?: (string | number)[];
videoUrl?: string;
voiceUrls?: string[];
};
// other party properties
}>;
for (const party of parties) {
// Extract inquiry.mapped for each party if needed, but we will keep the full vehicle object.
for (const party of typedParties) {
const mapped = party?.vehicle?.inquiry?.mapped;
// If you still want this field, you can add it like:
// party.inquiryMapped = mapped ?? null;
// But based on your last request, we are keeping the full vehicle object.
}
// 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;
}
// The line `delete party.vehicle;` has been removed, so vehicle will be included.
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);
@@ -997,6 +1103,124 @@ export class ExpertBlameService {
}
}
async submitInPerson(
requestId: string,
actorId: 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: 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,
@@ -1069,7 +1293,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();
@@ -1080,7 +1304,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.",

View File

@@ -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",
);
}
}
}