forked from Yara724/api
vehicle data added to get single request by expert , also submit in person
This commit is contained in:
@@ -1,19 +1,21 @@
|
|||||||
//! NEW
|
//! NEW
|
||||||
export enum CaseStatus {
|
export enum CaseStatus {
|
||||||
|
|
||||||
OPEN = "OPEN",
|
OPEN = "OPEN",
|
||||||
|
|
||||||
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
|
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
|
||||||
|
|
||||||
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
|
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
|
||||||
|
|
||||||
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
|
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
|
||||||
|
|
||||||
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
|
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
|
||||||
|
|
||||||
COMPLETED = "COMPLETED",
|
COMPLETED = "COMPLETED",
|
||||||
|
|
||||||
CANCELLED = "CANCELLED",
|
CANCELLED = "CANCELLED",
|
||||||
|
|
||||||
AUTO_CLOSED = "AUTO_CLOSED"
|
AUTO_CLOSED = "AUTO_CLOSED",
|
||||||
}
|
|
||||||
|
STOPPED = "STOPPED"
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ export class ExpertBlameService {
|
|||||||
private readonly expertDbService: ExpertDbService,
|
private readonly expertDbService: ExpertDbService,
|
||||||
private readonly blameDocumentDbService: BlameDocumentDbService,
|
private readonly blameDocumentDbService: BlameDocumentDbService,
|
||||||
private readonly userSignDbService: UserSignDbService,
|
private readonly userSignDbService: UserSignDbService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
async findAll(actor: any): Promise<AllRequestDtoRs> {
|
async findAll(actor: any): Promise<AllRequestDtoRs> {
|
||||||
// 1. Fetch all potentially relevant requests from the database.
|
// 1. Fetch all potentially relevant requests from the database.
|
||||||
@@ -426,7 +426,7 @@ export class ExpertBlameService {
|
|||||||
// The file is locked by someone else, or lock expired but status hasn't updated yet
|
// 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
|
// Only block if lock is still active and not by current expert
|
||||||
if (request.lockFile && !isLockExpired && !isLockedByCurrentExpert) {
|
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 non–CAR_BODY types. Builds file links for all evidence.
|
* 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.
|
* 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, actorId: string): Promise<Record<string, unknown>> {
|
||||||
try {
|
try {
|
||||||
const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
||||||
if (!doc) {
|
if (!doc) throw new NotFoundException("Request not found");
|
||||||
throw new NotFoundException("Request not found");
|
|
||||||
}
|
|
||||||
const type = doc.type as string;
|
const type = doc.type as string;
|
||||||
if (type === BlameRequestType.CAR_BODY) {
|
if (type === BlameRequestType.CAR_BODY) {
|
||||||
throw new ForbiddenException(
|
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 expertInitiated = doc.expertInitiated === true;
|
||||||
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
||||||
if (expertInitiated && initiatedByFieldExpertId && String(initiatedByFieldExpertId) !== actorId) {
|
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 decision = (doc.expert as any)?.decision;
|
||||||
const decidedByExpertId = decision?.decidedByExpertId;
|
const decidedByExpertId = decision?.decidedByExpertId;
|
||||||
if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
|
if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
|
||||||
@@ -553,61 +637,83 @@ export class ExpertBlameService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parties = (doc.parties ?? []) as Array<{
|
const parties = (doc.parties ?? []);
|
||||||
evidence?: { videoId?: string; voices?: string[] };
|
|
||||||
[key: string]: unknown;
|
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;
|
if (!party.evidence) continue;
|
||||||
const evidence = party.evidence as Record<string, unknown>;
|
const evidence = party.evidence as Record<string, unknown>;
|
||||||
|
|
||||||
if (evidence.videoId) {
|
if (evidence.videoId) {
|
||||||
const videoDoc = await this.blameVideoDbService.findById(
|
const videoDoc = await this.blameVideoDbService.findById(String(evidence.videoId));
|
||||||
String(evidence.videoId),
|
if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path);
|
||||||
);
|
|
||||||
if (videoDoc?.path) {
|
|
||||||
evidence.videoUrl = buildFileLink(videoDoc.path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evidence.voices && Array.isArray(evidence.voices)) {
|
if (evidence.voices && Array.isArray(evidence.voices)) {
|
||||||
const voiceUrls: string[] = [];
|
const voiceUrls: string[] = [];
|
||||||
for (const voiceId of evidence.voices) {
|
for (const voiceId of evidence.voices) {
|
||||||
const voiceDoc = await this.blameVoiceDbService.findById(
|
const voiceDoc = await this.blameVoiceDbService.findById(String(voiceId));
|
||||||
String(voiceId),
|
if (voiceDoc?.path) voiceUrls.push(buildFileLink(voiceDoc.path));
|
||||||
);
|
|
||||||
if (voiceDoc?.path) {
|
|
||||||
voiceUrls.push(buildFileLink(voiceDoc.path));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
evidence.voiceUrls = voiceUrls;
|
evidence.voiceUrls = voiceUrls;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdAt = doc.createdAt ? new Date(doc.createdAt as string | Date) : new Date();
|
// Date formatting
|
||||||
const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string | Date) : new Date();
|
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 [createdDate, createdTime] = toJalaliDateAndTime(createdAt);
|
||||||
const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
|
const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
|
||||||
|
|
||||||
doc.createdAtFormatted = `${createdDate} ${createdTime}`;
|
doc.createdAtFormatted = `${createdDate} ${createdTime}`;
|
||||||
doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`;
|
doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`;
|
||||||
|
|
||||||
// Exclude mapped inquiry vehicle for both parties from response
|
// The line `delete party.vehicle;` has been removed, so vehicle will be included.
|
||||||
for (const party of parties) {
|
|
||||||
delete party.vehicle;
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
|
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
"findOneV2 failed",
|
"findOneV2 failed",
|
||||||
requestId,
|
requestId,
|
||||||
error instanceof Error ? error.stack : String(error),
|
error instanceof Error ? error.stack : String(error),
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to get blame case details",
|
error instanceof Error ? error.message : "Failed to get blame case details",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async lockRequest(requestId: string, actorDetail) {
|
async lockRequest(requestId: string, actorDetail) {
|
||||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
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(
|
private async updateDamageExpertStats(
|
||||||
expertId: string,
|
expertId: string,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { ResendRequestDto } from "./dto/resend.dto";
|
|||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@Roles(RoleEnum.EXPERT, RoleEnum.FIELD_EXPERT)
|
@Roles(RoleEnum.EXPERT, RoleEnum.FIELD_EXPERT)
|
||||||
export class ExpertBlameV2Controller {
|
export class ExpertBlameV2Controller {
|
||||||
constructor(private readonly expertBlameService: ExpertBlameService) {}
|
constructor(private readonly expertBlameService: ExpertBlameService) { }
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(@CurrentUser() actor: any) {
|
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