Initial commit after migration to gitea

This commit is contained in:
2026-01-18 11:27:43 +03:30
parent a21039410c
commit ea4b8eb543
196 changed files with 45567 additions and 9 deletions

View File

@@ -0,0 +1,103 @@
import { RequestManagementModel } from "src/request-management/entities/schema/request-management.schema";
export class AllRequestDto {
// TODO fix interface for class
firstPartyPlate: object;
secondPartyPlate: object;
secondPartyCar: string | undefined;
firstPartyCar: string | undefined;
requestId: string;
submitTime: string;
requestCode: string;
date: Date | string;
time: Date | string;
firstPartyDetail: Object | undefined;
secondPartyDetail: Object | undefined;
requestStatus: string;
lockFile: boolean | undefined;
lockTime: any;
userComment: boolean | null;
status: string;
partiesInitialForms: Object;
type: string;
constructor(request: RequestManagementModel) {
// TODO FIX THIS OBJECT AND CLASS FOR CLEAN CODE
this.requestId = request["_id"];
this.status = request.blameStatus;
this.userComment = this.userCommentVoid(request);
this.requestCode = request.requestNumber;
// Format date and time with Iran timezone (Asia/Tehran)
if (request.createdAt) {
const dateFormatOptions: Intl.DateTimeFormatOptions = {
timeZone: "Asia/Tehran",
year: "numeric",
month: "2-digit",
day: "2-digit",
};
const timeFormatOptions: Intl.DateTimeFormatOptions = {
timeZone: "Asia/Tehran",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
this.date = `${new Date(request.createdAt).toLocaleDateString("fa-IR", dateFormatOptions)} `;
this.time = new Date(request.createdAt).toLocaleTimeString("fa-IR", timeFormatOptions);
} else {
this.date = "";
this.time = "";
}
this.lockFile = request.lockFile;
this.lockTime = request.lockTime;
this.type = request.type || "THIRD_PARTY"; // Include type field, default to THIRD_PARTY for backward compatibility
this.partiesInitialForms = {
firstParty: Object.entries(
request.firstPartyDetails.firstPartyInitialForm,
)
.filter(([key, value]) => value)
.flat()[0],
secondParty: Object.entries(
request.secondPartyDetails.secondPartyInitialForm,
)
.filter(([key, value]) => (value ? key : null))
.flat()[0],
};
this.firstPartyCar =
request.firstPartyDetails?.firstPartyCarDetail?.carName;
this.secondPartyCar =
request.secondPartyDetails?.secondPartyCarDetail?.carName;
}
userCommentVoid(request: RequestManagementModel): boolean | null {
if (
request?.expertSubmitReply?.firstPartyComment &&
request?.expertSubmitReply?.secondPartyComment
) {
if (
request.expertSubmitReply.firstPartyComment.isAccept ||
request.expertSubmitReply.secondPartyComment.isAccept
) {
return true;
} else {
return false;
}
} else {
return null;
}
}
getFirstValidKey(obj: Record<string, boolean>) {
return (
Object.entries(obj).find(([_, value]) => value === true)?.[0] ?? null
);
}
}
export class AllRequestDtoRs {
public data;
constructor(requests: RequestManagementModel[]) {
this.data = requests.map((r) => new AllRequestDto(r));
}
}

View File

@@ -0,0 +1,72 @@
import { ApiProperty } from "@nestjs/swagger";
import { Types } from "mongoose";
import { BlameDocumentType } from "src/request-management/entities/schema/blame-document.schema";
export class AccidentWayIF {
@ApiProperty({ required: true })
id: string;
@ApiProperty({ required: true })
label: string;
}
export class AccidentReasonIF {
@ApiProperty({ required: true })
id: string;
@ApiProperty({ required: true })
label: string;
@ApiProperty({ required: true })
fanavaran: number;
}
export class FieldsInterface {
@ApiProperty({ required: true })
accidentWay: AccidentWayIF;
@ApiProperty({ required: true })
accidentReason: AccidentReasonIF;
@ApiProperty({ required: true })
accidentType: AccidentWayIF;
}
export class SubmitReplyDto {
@ApiProperty({ required: true })
description: string;
@ApiProperty({ required: true, type: Types.ObjectId })
guiltyUserId: Types.ObjectId;
@ApiProperty({ required: true })
fields: FieldsInterface;
}
export class ResendFirstPartyDto {
voice?: string;
userReply?: string;
@ApiProperty({ required: false })
firstPartyId: string | null;
@ApiProperty({ required: false })
firstPartyDescription: string | null;
documents?: { [key in BlameDocumentType]?: Types.ObjectId | string };
}
export class ResendSecondPartyDto {
voice?: string;
userReply?: string;
@ApiProperty({ required: false })
secondPartyId: string | null;
@ApiProperty({ required: false })
secondPartyDescription: string | null;
documents?: { [key in BlameDocumentType]?: Types.ObjectId | string };
}
export interface SendAginIF {
first: ResendFirstPartyDto;
second: ResendSecondPartyDto;
}

View File

@@ -0,0 +1,156 @@
import {
Controller,
Get,
Body,
Param,
UseGuards,
Put,
Req,
Res,
Headers,
UseInterceptors,
Patch,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiBody,
ApiOkResponse,
ApiParam,
ApiProduces,
ApiTags,
} from "@nestjs/swagger";
import { Response, Request } from "express";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { ClientKey } from "src/decorators/clientKey.decorator";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { LoggingInterceptor } from "src/interceptor/logging.interceptors";
import { RoleEnum } from "src/Types&Enums/role.enum";
import {
ResendFirstPartyDto,
ResendSecondPartyDto,
SendAginIF,
SubmitReplyDto,
} from "./dto/reply.dto";
import { ExpertBlameService } from "./expert-blame.service";
@ApiTags("expert-blame-panel")
@Controller("expert-blame")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.EXPERT)
export class ExpertBlameController {
constructor(private readonly expertBlameService: ExpertBlameService) {}
// TODO role guard for expert fix
@Roles(RoleEnum.EXPERT)
@Get()
async findAll(@CurrentUser() actor, @ClientKey() client) {
return await this.expertBlameService.findAll(actor);
}
@Roles(RoleEnum.EXPERT)
@Get(":id")
async findOne(@Param("id") id: string, @CurrentUser() actor) {
return await this.expertBlameService.findOne(id, actor.sub);
}
@Roles(RoleEnum.EXPERT)
@Put("lock/:id")
async lockRequest(@Param("id") id: string, @CurrentUser() actor) {
return await this.expertBlameService.lockRequest(id, actor);
}
@Roles(RoleEnum.EXPERT)
@Get("request/accident-fields")
async getAccidentFields() {
return await this.expertBlameService.getAccidentField();
}
@Roles(RoleEnum.EXPERT)
@Put("reply/submit/:id")
@ApiBody({ type: SubmitReplyDto })
@ApiParam({ name: "id" })
async submitReply(
@Param("id") id: string,
@Body() body: SubmitReplyDto,
@CurrentUser() actor,
) {
return await this.expertBlameService.replyRequest(id, body, actor.sub);
}
private async handleResendRequest(
id: string,
body: SendAginIF,
actorSub: string,
req: Request,
) {
return await this.expertBlameService.sendAgainRequest(
id,
body,
actorSub,
req,
);
}
@Roles(RoleEnum.EXPERT)
@Put("reply/resend/first/:id")
@ApiBody({ type: ResendFirstPartyDto })
@ApiParam({ name: "id" })
async resendFirstParty(
@Param("id") id: string,
@Body() body: SendAginIF,
@CurrentUser() actor,
@Req() req: Request,
) {
return this.handleResendRequest(id, body, actor.sub, req);
}
@Roles(RoleEnum.EXPERT)
@Put("reply/resend/second/:id")
@Roles(RoleEnum.EXPERT)
@ApiBody({ type: ResendSecondPartyDto })
@ApiParam({ name: "id" })
async resendSecondParty(
@Param("id") id: string,
@Body() body: SendAginIF,
@CurrentUser() actor,
@Req() req: Request,
) {
return this.handleResendRequest(id, body, actor.sub, req);
}
@Roles(RoleEnum.EXPERT)
@Get("stream/:requestId")
@ApiParam({ name: "requestId" })
async streamVideo(@Param("requestId") requestId: string) {
return this.expertBlameService.streamVideo(requestId);
}
@UseInterceptors(LoggingInterceptor)
@Get("voice/:requestId/:voiceId")
@Roles(RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
@ApiParam({ name: "requestId" })
@ApiParam({ name: "voiceId" })
@ApiOkResponse({
schema: {
type: "string",
format: "binary",
},
})
@ApiProduces("mp3")
async downloadVoice(
@Param("voiceId") voiceId,
@Param("requestId") requestId: string,
@Res({ passthrough: true }) res: Response,
@Req() req: Request,
@Headers() headers,
) {
return await this.expertBlameService.streamVoice(requestId, voiceId);
}
@ApiParam({ name: "id" })
@Patch(":id/visit")
async inPersonVisit(@Param("id") requestId: string, @CurrentUser() actor) {
return await this.expertBlameService.inPersonVisit(requestId, actor);
}
}

View File

@@ -0,0 +1,15 @@
import { Module } from "@nestjs/common";
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 { ExpertBlameService } from "./expert-blame.service";
import { RequestManagementModule } from "src/request-management/request-management.module";
@Module({
imports: [UsersModule, ClientModule, RequestManagementModule, PlatesModule],
controllers: [ExpertBlameController],
providers: [ExpertBlameService],
exports: [ExpertBlameService],
})
export class ExpertBlameModule {}

View File

@@ -0,0 +1,730 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
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 { UserType } from "src/Types&Enums/userType.enum";
import { SubmitReplyDto } from "./dto/reply.dto";
import { Types } from "mongoose";
import { BlameVideoDbService } from "src/request-management/entities/db-service/blame-video.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 { buildFileLink } from "src/helpers/urlCreator";
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";
interface CheckedRequestEntry {
CheckedRequest?: {
actorId: string;
fullName: string;
};
[key: string]: any;
}
@Injectable()
export class ExpertBlameService {
private readonly logger = new Logger(ExpertBlameService.name);
constructor(
private readonly requestManagementDbService: RequestManagementDbService,
private readonly clientDbService: ClientDbService,
private readonly blameVideoDbService: BlameVideoDbService,
private readonly blameVoiceDbService: BlameVoiceDbService,
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.
// Exclude CAR_BODY type requests as they are automatically handled and don't need expert review
const allRequests = await this.requestManagementDbService.findAll({
"firstPartyDetails.firstPartyPlate": { $ne: null },
"secondPartyDetails.secondPartyPlate": { $ne: null },
blameStatus: {
$in: [
ReqBlameStatus.UnChecked,
ReqBlameStatus.CloseRequest,
ReqBlameStatus.CheckAgain,
ReqBlameStatus.ReviewRequest,
],
},
// Both parties must have submitted their initial forms
"firstPartyDetails.firstPartyInitialForm": { $exists: true },
"secondPartyDetails.secondPartyInitialForm": { $exists: true },
type: { $ne: "CAR_BODY" }, // Exclude CAR_BODY type requests
});
// 2. Filter requests that need expert review based on initial form logic
// Expert is needed when there's a conflict (both claim damaged, both claim guilty, etc.)
// Expert is NOT needed when one says imDamaged and the other says imGuilty (auto-resolved)
const requestsNeedingExpert = [];
for (const request of allRequests) {
const firstPartyForm = request.firstPartyDetails?.firstPartyInitialForm;
const secondPartyForm = request.secondPartyDetails?.secondPartyInitialForm;
if (!firstPartyForm || !secondPartyForm) {
continue; // Skip if forms are not complete
}
// Check if this can be auto-resolved (one says damaged, other says guilty)
const canAutoResolve =
(firstPartyForm.imDamaged && secondPartyForm.imGuilty) ||
(secondPartyForm.imDamaged && firstPartyForm.imGuilty);
// If it can be auto-resolved, skip it (no expert needed)
if (canAutoResolve) {
continue;
}
// Otherwise, expert is needed (both damaged, both guilty, or other conflicts)
requestsNeedingExpert.push(request);
}
// 3. Filter the requests in memory based on the expert's specific access rights.
const visibleRequests = [];
for (const request of requestsNeedingExpert) {
// For expert-initiated files, only show to the initiating expert
if (request.expertInitiated && request.initiatedBy) {
if (String(request.initiatedBy) !== actor.sub) {
continue; // Skip if not the initiating expert
}
// Expert-initiated files are always visible to the initiating expert
visibleRequests.push(request);
continue;
}
// For normal files, use existing client-based filtering
const firstPartyClientId =
request.firstPartyDetails?.firstPartyClient?.clientId?.toString();
const secondPartyClientId =
request.secondPartyDetails?.secondPartyClient?.clientId?.toString();
const partyClientIds = [firstPartyClientId, secondPartyClientId]
.filter(Boolean)
.map((id) => new Types.ObjectId(id));
if (partyClientIds.length === 0) {
continue;
}
let clientQuery: any = { _id: { $in: partyClientIds } };
if (actor.userType === UserType.LEGAL) {
clientQuery = {
$and: [
{ _id: { $in: partyClientIds } },
{ _id: new Types.ObjectId(actor.clientKey) },
],
};
}
const client = await this.clientDbService.findOne(clientQuery);
if (!client) {
continue;
}
const isExpertTypeMatch = client.useExpertMode === actor.userType;
if (!isExpertTypeMatch) {
continue;
}
if (request.blameStatus === ReqBlameStatus.CheckAgain) {
if (String(request.actorLocked?.actorId) === actor.sub) {
visibleRequests.push(request);
}
} else {
visibleRequests.push(request);
}
}
return new AllRequestDtoRs(visibleRequests);
}
public unlockApi(request, timer) {
return setTimeout(async () => {
try {
const r = await this.requestManagementDbService.findOne(request._id);
const updateExp: any = {
lockFile: false,
unlockTime: null,
};
const shouldDecrementChecked =
r.blameStatus === ReqBlameStatus.ReviewRequest &&
!r.expertSubmitReply &&
r.actorLocked?.actorId;
if (shouldDecrementChecked) {
updateExp.blameStatus = ReqBlameStatus.UnChecked;
await this.expertDbService.findOneAndUpdate(
{ _id: new Types.ObjectId(r.actorLocked.actorId) },
{
$inc: { "requestStats.totalChecked": -1 },
$pull: { countedRequests: r._id.toString() },
},
);
this.logger.warn(
`Request ${r._id} unlocked without reply — expert stats rolled back.`,
);
}
await this.requestManagementDbService.findByIdAndUpdate(
r._id.toString(),
updateExp,
);
this.logger.log(`Unlock completed for request: ${r._id}`);
} catch (error) {
this.logger.error(`Failed to unlock request ${request._id}`, error);
}
}, timer);
}
public scheduleUnlock(request) {
const unlockDelay = new Date(request.unlockTime).getTime() - Date.now();
if (unlockDelay <= 0) return; // already expired
setTimeout(async () => {
try {
// Double-check latest state before unlocking
const current = await this.requestManagementDbService.findOne(
request._id,
);
if (!current.lockFile || current.expertSubmitReply) {
// Already unlocked or replied
return;
}
// If expiry passed
if (current.unlockTime && new Date(current.unlockTime) <= new Date()) {
const shouldRollbackStats =
current.blameStatus === ReqBlameStatus.ReviewRequest &&
!current.expertSubmitReply &&
current.actorLocked?.actorId;
const update: any = {
lockFile: false,
unlockTime: null,
lockTime: null,
};
if (shouldRollbackStats) {
update.blameStatus = ReqBlameStatus.UnChecked;
await this.expertDbService.findOneAndUpdate(
{ _id: new Types.ObjectId(current.actorLocked.actorId) },
{
$inc: { "requestStats.totalChecked": -1 },
$pull: { countedRequests: current._id.toString() },
},
);
this.logger.warn(
`Request ${current._id} auto-unlocked (no reply) — expert stats rolled back.`,
);
}
await this.requestManagementDbService.findByIdAndUpdate(
String(current._id),
update,
);
this.logger.log(`Auto-unlock completed for request: ${current._id}`);
}
} catch (err) {
this.logger.error(`Auto-unlock failed for ${request._id}`, err);
}
}, unlockDelay);
}
async findOne(requestId: string, actorId: string) {
// 1. Fetch the main request document
const request = await this.requestManagementDbService.findOne(requestId);
if (!request) {
throw new NotFoundException("Request not found");
}
// 1.5. Reject CAR_BODY type requests as they don't need expert review
if (request.type === "CAR_BODY") {
throw new ForbiddenException(
"CAR_BODY type requests are automatically handled and do not require expert review.",
);
}
// 2. Initial validation to ensure the expert has access
if (String(request?.actorLocked?.actorId) === actorId && request.lockFile) {
// This is the correct expert, and the file is locked to them, which is fine.
} else if (
request.lockFile ||
request.blameStatus === ReqBlameStatus.ReviewRequest
) {
// The file is locked, but not by the current expert.
throw new BadRequestException("Request is locked by another expert");
}
// 3. Populate the resend links if the data exists
if (request.expertResendReply) {
const populatePartyLinks = async (
partyKey: "firstParty" | "secondParty",
) => {
const partyReply = request.expertResendReply[partyKey];
if (!partyReply) return;
// Populate the voice link
if (partyReply.voice) {
const voiceDoc = await this.userSignDbService.findById(
partyReply.voice.toString(),
);
if (voiceDoc) {
partyReply.voice = buildFileLink(voiceDoc.path);
}
}
// Populate the document links
if (partyReply.documents) {
for (const docType in partyReply.documents) {
const docId = partyReply.documents[docType];
if (docId) {
const doc = await this.blameDocumentDbService.findById(
docId.toString(),
);
if (doc) {
partyReply.documents[docType] = buildFileLink(doc.path); // Replace ID with URL
}
}
}
}
};
await populatePartyLinks("firstParty");
await populatePartyLinks("secondParty");
}
// 4. Populate the Signature Links from the correct reply object
// First, determine which reply object is the final, authoritative one.
const finalReply =
request.expertSubmitReplyFinal || request.expertSubmitReply;
if (finalReply) {
const populateSignatureLink = async (
commentField: "firstPartyComment" | "secondPartyComment",
) => {
const comment = finalReply[commentField];
// Check if the comment and its signDetail with a fileId exist
if (comment?.signDetail?.fileId) {
const signDoc = await this.userSignDbService.findById(
comment.signDetail.fileId.toString(),
);
if (signDoc) {
// Add a new 'fileUrl' property to the signDetail object
(comment.signDetail as any).fileUrl = buildFileLink(signDoc.path);
}
}
};
// Run the population for both parties' signatures on the correct reply object.
await populateSignatureLink("firstPartyComment");
await populateSignatureLink("secondPartyComment");
}
// 5. Format the date for display with Iran timezone (Asia/Tehran)
if (request.createdAt) {
const formattingOptions: Intl.DateTimeFormatOptions = {
timeZone: "Asia/Tehran",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
request.createdAt = new Date(request.createdAt).toLocaleString(
"fa-IR",
formattingOptions,
);
}
// 6. Return the fully populated request object
return request;
}
async lockRequest(requestId: string, actorDetail) {
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
const updateResult = await this.requestManagementDbService.findOneAndUpdate(
{
_id: requestId,
lockFile: false,
blameStatus: { $ne: ReqBlameStatus.UserPending },
},
{
$set: {
lockFile: true,
blameStatus: ReqBlameStatus.ReviewRequest,
unlockTime: fifteenMinutes,
lockTime: new Date(),
actorLocked: {
fullName: actorDetail.fullName,
actorId: new Types.ObjectId(actorDetail.sub),
},
},
$push: {
actorsChecker: {
[ReqBlameStatus.ReviewRequest]: {
fullName: actorDetail.fullName,
actorId: new Types.ObjectId(actorDetail.sub),
},
Date: new Date(),
},
},
},
{ new: true },
);
if (!updateResult) {
throw new BadRequestException("Request already locked or invalid status");
}
// Update expert stats atomically (use findOneAndUpdate with conditions)
await this.updateDamageExpertStats(actorDetail.sub, requestId, "checked");
this.scheduleUnlock(updateResult);
return { _id: requestId, lock: true };
}
private async updateDamageExpertStats(
expertId: string,
requestId: string,
type: "checked" | "handled",
) {
if (!expertId || !requestId || !["checked", "handled"].includes(type)) {
console.warn("Invalid expertId, requestId, or type");
return;
}
const expert = await this.expertDbService.findOne({
_id: new Types.ObjectId(expertId),
});
if (!expert) {
console.warn("Expert not found:", expertId);
return;
}
const requestIdStr = new Types.ObjectId(requestId).toString();
const countedRequestIds =
expert.countedRequests?.map((id) => id.toString()) || [];
if (type === "checked" && countedRequestIds.includes(requestIdStr)) {
console.log(
`Request ${requestIdStr} already checked for expert ${expertId}`,
);
return;
}
const update: any = { $inc: {}, $push: {} };
if (type === "checked") {
update.$inc["requestStats.totalChecked"] = 1;
update.$push["countedRequests"] = requestIdStr;
} else if (type === "handled") {
update.$inc["requestStats.totalHandled"] = 1;
if (countedRequestIds.includes(requestIdStr)) {
update.$inc["requestStats.totalChecked"] = -1;
}
if (!countedRequestIds.includes(requestIdStr)) {
update.$push["countedRequests"] = requestIdStr;
} else {
delete update.$push;
}
}
const updateResult = await this.expertDbService.findOneAndUpdate(
{ _id: new Types.ObjectId(expertId) },
update,
);
if (!updateResult) {
console.warn("Failed to update expert stats for:", expertId);
} else {
console.log(`Expert stats updated (${type}) for expert:`, expertId);
}
}
async replyRequest(requestId: string, reply: SubmitReplyDto, userId: string) {
const request = await this.requestManagementDbService.findOne(requestId);
if (!request) {
throw new NotFoundException("Request not found");
}
if (String(request.actorLocked?.actorId) !== userId) {
throw new ForbiddenException(
"Access denied to this request. You are not the locked expert.",
);
}
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.",
);
}
const isObjection = !!request.expertResendReply;
const replyField = isObjection
? "expertSubmitReplyFinal"
: "expertSubmitReply";
if (!isObjection && request.expertSubmitReply) {
throw new ForbiddenException(
"This request already has an initial expert reply.",
);
}
if (isObjection && request.expertSubmitReplyFinal) {
throw new ForbiddenException(
"This request already has a final expert reply.",
);
}
const newReplyObject = {
description: reply.description,
submitTime: new Date(),
guiltyUserId: reply.guiltyUserId,
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,
},
},
firstPartyComment: request.expertSubmitReply?.firstPartyComment || null,
secondPartyComment: request.expertSubmitReply?.secondPartyComment || null,
};
const updatePayload: any = {
$set: {
lockFile: false,
blameStatus: ReqBlameStatus.CheckedRequest,
[replyField]: newReplyObject,
},
$push: {
actorsChecker: {
[ReqBlameStatus.CheckedRequest]: request.actorLocked,
Date: new Date(),
},
},
};
if (isObjection) {
updatePayload.$set.expertSubmitReply = newReplyObject;
}
try {
await this.requestManagementDbService.findAndUpdate(
{ _id: requestId },
updatePayload,
);
return {
requestId: request._id,
blameStatus: ReqBlameStatus.CheckedRequest,
};
} catch (error) {
this.logger.error("Failed to submit expert reply:", error);
throw new Error("Failed to submit expert reply");
}
}
async sendAgainRequest(
requestId: string,
resend: any,
userId: string,
req: any,
) {
const request = await this.requestManagementDbService.findOne(requestId);
if (!request) {
throw new NotFoundException("Request not found");
}
if (String(request.actorLocked?.actorId) !== userId) {
throw new ForbiddenException("Access denied to this request");
}
if (request.expertSubmitReply) {
throw new ForbiddenException("Request already has an expert reply");
}
if (request.unlockTime == null) {
throw new ForbiddenException("Your lock time has expired or was not set");
}
const partyType = req.route.path.split("/")[4];
switch (partyType) {
case "first": {
if (request.expertResendReply?.firstParty) {
throw new ForbiddenException(
"Request has an expert resend reply for the first party",
);
}
const { firstPartyId, firstPartyDescription } = resend;
try {
await this.requestManagementDbService.findAndUpdate(
{ _id: requestId },
{
lockFile: false,
blameStatus: ReqBlameStatus.UserPending,
"expertResendReply.firstParty.firstPartyId": firstPartyId,
"expertResendReply.firstParty.firstPartyDescription":
firstPartyDescription,
$push: {
actorsChecker: {
[ReqBlameStatus.UserPending]: request.actorLocked,
Date: new Date(),
},
},
},
);
} catch (error) {
this.logger.error("Failed to update for first party:", error);
throw error;
}
return {
requestId: request._id,
blameStatus: ReqBlameStatus.UserPending,
};
}
case "second": {
if (request.expertResendReply?.secondParty) {
throw new ForbiddenException(
"Request has an expert resend reply for the second party",
);
}
const { secondPartyId, secondPartyDescription } = resend;
try {
await this.requestManagementDbService.findAndUpdate(
{ _id: requestId },
{
lockFile: false,
blameStatus: ReqBlameStatus.UserPending,
"expertResendReply.secondParty.secondPartyId": secondPartyId,
"expertResendReply.secondParty.secondPartyDescription":
secondPartyDescription,
$push: {
actorsChecker: {
[ReqBlameStatus.UserPending]: request.actorLocked,
Date: new Date(),
},
},
},
);
} catch (error) {
this.logger.error("Failed to update for second party:", error);
throw error;
}
// TODO notification for user parties
// TODO send SMS notification
// TODO send URI For USER
return {
requestId: request._id,
blameStatus: ReqBlameStatus.UserPending,
};
}
default:
throw new BadRequestException(
`Invalid party type in URL: ${partyType}`,
);
}
}
/// VIDEO SERVICE && VOICE SERVICE
// TODO add video service to Object Storage
async streamVideo(requestId): Promise<string> {
const request = await this.requestManagementDbService.findOne(requestId);
const video_path = await this.blameVideoDbService.findOne(
String(request.firstPartyDetails.firstPartyFile.firstPartyVideoId),
);
return buildFileLink(video_path.path);
}
async streamVoice(requestId, voiceId) {
try {
const voice = await this.blameVoiceDbService.findOne(voiceId);
if (!voice) throw new NotFoundException("not found voice");
if (String(voice.requestId) === requestId) {
return buildFileLink(voice.path);
} else {
throw new ForbiddenException(
"Can Not Access To This Voice Because Voice is Not Assign to RequestID",
);
}
} catch (er) {
if (er) throw new NotFoundException("voice not found ", er);
}
}
async getAccidentField() {
try {
const ac_reason = await readFile(
"src/static/ACCIDENT_REASON.json",
"utf-8",
);
const ac_type = await readFile("src/static/ACCIDENT_TYPE.json", "utf-8");
const ac_way = await readFile("src/static/ACCIDENT_WAY.json", "utf-8");
return {
accidentReason: JSON.parse(ac_reason),
accidentType: JSON.parse(ac_type),
accidentWay: JSON.parse(ac_way),
};
} catch (err) {
this.logger.error(err);
}
}
async inPersonVisit(requestId: string, actorDetail: any) {
const request = await this.requestManagementDbService.findOne(requestId);
if (!request) {
throw new NotFoundException("Blame not found");
}
const updated = await this.requestManagementDbService.findAndUpdate(
{ _id: new Types.ObjectId(requestId) },
{
blameStatus: ReqBlameStatus.InPersonVisit,
},
);
await this.expertDbService.updateStats(
actorDetail.sub,
"handled",
requestId,
);
return updated;
}
}