forked from Yara724/api
3213 lines
105 KiB
TypeScript
3213 lines
105 KiB
TypeScript
import {
|
||
BadGatewayException,
|
||
BadRequestException,
|
||
ConflictException,
|
||
ForbiddenException,
|
||
HttpException,
|
||
HttpStatus,
|
||
Injectable,
|
||
InternalServerErrorException,
|
||
Logger,
|
||
NotFoundException,
|
||
} from "@nestjs/common";
|
||
import { ExternalExceptionFilter } from "@nestjs/core/exceptions/external-exception-filter";
|
||
import { Types } from "mongoose";
|
||
import ShortUniqueId from "short-unique-id";
|
||
import { ClientService } from "src/client/client.service";
|
||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||
import {
|
||
CarBodyFormDto,
|
||
CarBodySecondForm,
|
||
InitialFormDto,
|
||
} from "src/request-management/dto/create-request-management.dto";
|
||
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 { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||
import { SandHubService } from "src/sand-hub/sand-hub.service";
|
||
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
|
||
import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
||
import { AutoCloseRequestService } from "src/utils/cron/cron.service";
|
||
import { SmsManagerService } from "src/utils/sms-manager/sms-manager.service";
|
||
import {
|
||
DescriptionDto,
|
||
LocationDto,
|
||
RequestManagementDtoRs,
|
||
} from "./dto/create-request-management.dto";
|
||
import { DetailsReplyDtoRs, UserReplyDto } from "./dto/user-reply.dto";
|
||
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
||
import { UserSignDbService } from "./entities/db-service/sign.db.service";
|
||
import { RequestManagementModel } from "./entities/schema/request-management.schema";
|
||
import { UploadContext } from "./entities/schema/blame-voice.schema";
|
||
import { BlameDocumentType } from "./entities/schema/blame-document.schema";
|
||
import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service";
|
||
import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service";
|
||
import {
|
||
CreationMethod,
|
||
FilledBy,
|
||
FileHistoryEvent,
|
||
ExpertLinkInfo,
|
||
} from "./entities/schema/request-management.schema";
|
||
import {
|
||
CreateExpertInitiatedFileDto,
|
||
} from "./dto/expert-initiated.dto";
|
||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||
|
||
@Injectable()
|
||
export class RequestManagementService {
|
||
private readonly logger = new Logger(RequestManagementService.name);
|
||
|
||
constructor(
|
||
private readonly requestManagementDbService: RequestManagementDbService,
|
||
private readonly blameDocumentDbService: BlameDocumentDbService,
|
||
private readonly blameVideoDbService: BlameVideoDbService,
|
||
private readonly userDbService: UserDbService,
|
||
private readonly sandHubService: SandHubService,
|
||
private readonly sandHubDbService: SandHubDbService,
|
||
private readonly clientService: ClientService,
|
||
private readonly blameVoiceDbService: BlameVoiceDbService,
|
||
private readonly userSign: UserSignDbService,
|
||
private readonly smsManagerService: SmsManagerService,
|
||
private readonly expertDbService: ExpertDbService,
|
||
private readonly autoCloseRequestService: AutoCloseRequestService,
|
||
private readonly claimRequestManagementDbService: ClaimRequestManagementDbService,
|
||
) {}
|
||
|
||
async createRequest(user, type) {
|
||
const reqNumber = new ShortUniqueId({ counter: 1 });
|
||
const createRequest = await this.requestManagementDbService.create({
|
||
firstPartyDetails: {
|
||
firstPartyId: user.sub,
|
||
firstPartyPhoneNumber: user.username,
|
||
firstPartyCertificate: null,
|
||
},
|
||
currentStep: StepsEnum.createRequest,
|
||
nextStep:
|
||
type === "THIRD_PARTY"
|
||
? StepsEnum.F_InitialForm
|
||
: StepsEnum.CarBodyForm,
|
||
requestNumber: reqNumber.rnd(),
|
||
blameStatus: ReqBlameStatus.PendingForFirstParty,
|
||
lockFile: false,
|
||
type,
|
||
});
|
||
return { requestId: createRequest["_id"] };
|
||
}
|
||
|
||
async initialFormStep(
|
||
reqBody: InitialFormDto,
|
||
user,
|
||
requestId,
|
||
): Promise<RequestManagementDtoRs> {
|
||
const foundRequest =
|
||
await this.requestManagementDbService.findOne(requestId);
|
||
|
||
if (!foundRequest) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// For LINK-based expert-initiated files, verify user is the correct party
|
||
if (foundRequest.expertInitiated && foundRequest.creationMethod === CreationMethod.LINK) {
|
||
// User should already be set in firstPartyDetails or secondPartyDetails
|
||
const isFirstParty = foundRequest.firstPartyDetails?.firstPartyId?.toString() === user.sub ||
|
||
foundRequest.firstPartyDetails?.firstPartyPhoneNumber === user.username;
|
||
const isSecondParty = foundRequest.secondPartyDetails?.secondPartyId?.toString() === user.sub ||
|
||
foundRequest.secondPartyDetails?.secondPartyPhoneNumber === user.username;
|
||
|
||
if (!isFirstParty && !isSecondParty) {
|
||
throw new ForbiddenException(
|
||
"You are not authorized to complete this file. This file was created by an expert for specific parties.",
|
||
);
|
||
}
|
||
}
|
||
|
||
if (
|
||
foundRequest?.secondPartyDetails?.secondPartyPhoneNumber == user.username
|
||
) {
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
"secondPartyDetails.secondPartyId": user.sub,
|
||
blameStatus: ReqBlameStatus.PendingForSecondParty,
|
||
"secondPartyDetails.secondPartyInitialForm": reqBody,
|
||
currentStep: StepsEnum.S_InitialForm,
|
||
nextStep: StepsEnum.S_addPlate,
|
||
$push: { steps: StepsEnum.S_InitialForm },
|
||
},
|
||
);
|
||
|
||
// Add history for expert-initiated files
|
||
if (foundRequest.expertInitiated) {
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"SECOND_PARTY_FORM_COMPLETED",
|
||
new Types.ObjectId(user.sub),
|
||
undefined,
|
||
foundRequest.filledBy === FilledBy.EXPERT ? "expert" : "customer",
|
||
);
|
||
}
|
||
|
||
return new RequestManagementDtoRs(
|
||
foundRequest.firstPartyDetails.firstPartyId,
|
||
user.sub,
|
||
StepsEnum.S_InitialForm,
|
||
StepsEnum.S_addPlate,
|
||
StepsEnum.S_addPlate,
|
||
requestId,
|
||
);
|
||
} else if (
|
||
foundRequest?.firstPartyDetails?.firstPartyPhoneNumber == user.username ||
|
||
(foundRequest.expertInitiated &&
|
||
foundRequest.creationMethod === CreationMethod.LINK &&
|
||
foundRequest.firstPartyDetails?.firstPartyId?.toString() === user.sub)
|
||
) {
|
||
// Normal flow or link-based - set full firstPartyDetails
|
||
const updatePayload: any = {
|
||
blameStatus: ReqBlameStatus.PendingForFirstParty,
|
||
firstPartyDetails: {
|
||
firstPartyId: user.sub,
|
||
firstPartyPhoneNumber: user.username,
|
||
firstPartyInitialForm: reqBody,
|
||
},
|
||
currentStep: StepsEnum.F_InitialForm,
|
||
nextStep: StepsEnum.F_videoUpload,
|
||
$push: { steps: StepsEnum.F_InitialForm },
|
||
};
|
||
|
||
// For link-based expert-initiated files, ensure filledBy is set
|
||
if (foundRequest.expertInitiated && foundRequest.creationMethod === CreationMethod.LINK) {
|
||
updatePayload.$set = { filledBy: FilledBy.CUSTOMER };
|
||
}
|
||
|
||
const createRequestFile =
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
updatePayload,
|
||
);
|
||
|
||
// Add history for expert-initiated link-based files
|
||
if (foundRequest.expertInitiated && foundRequest.creationMethod === CreationMethod.LINK) {
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"FIRST_PARTY_FORM_COMPLETED",
|
||
new Types.ObjectId(user.sub),
|
||
undefined,
|
||
"customer",
|
||
);
|
||
}
|
||
|
||
return new RequestManagementDtoRs(
|
||
createRequestFile.firstPartyDetails.firstPartyId,
|
||
null,
|
||
StepsEnum.F_InitialForm,
|
||
StepsEnum.F_videoUpload,
|
||
"Add video",
|
||
createRequestFile._id,
|
||
);
|
||
} else {
|
||
// If none of the conditions match, throw an error
|
||
throw new ForbiddenException(
|
||
"You are not authorized to complete this form. Please ensure you are the correct party or using a valid link.",
|
||
);
|
||
}
|
||
}
|
||
|
||
async videoUploadStep(
|
||
file,
|
||
user,
|
||
requestId,
|
||
): Promise<RequestManagementDtoRs> {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// Check phone number match for normal flow
|
||
if (request?.firstPartyDetails?.firstPartyPhoneNumber !== user.username) {
|
||
throw new ForbiddenException("the request doesnt belong you");
|
||
}
|
||
|
||
try {
|
||
if (request.steps.includes(StepsEnum.F_videoUpload) || request.steps.includes(StepsEnum.CarBodyVideo))
|
||
throw new BadRequestException("video steps has exist");
|
||
|
||
const videoDocument = await this.blameVideoDbService.create({
|
||
fileName: file.filename,
|
||
path: file.path,
|
||
requestId,
|
||
});
|
||
|
||
// Determine the step to push and next step based on request type
|
||
const stepToPush = request.type === "THIRD_PARTY" ? StepsEnum.F_videoUpload : StepsEnum.CarBodyVideo;
|
||
const currentStepValue = request.type === "THIRD_PARTY" ? StepsEnum.F_videoUpload : StepsEnum.CarBodyVideo;
|
||
const nextStepValue = StepsEnum.F_addPlate;
|
||
|
||
const updated = await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
$push: { steps: stepToPush },
|
||
currentStep: currentStepValue,
|
||
"firstPartyDetails.firstPartyFile.firstPartyVideoId":
|
||
videoDocument["_doc"]._id,
|
||
nextStep: nextStepValue,
|
||
},
|
||
);
|
||
|
||
if (updated == undefined) return new BadGatewayException("update failed");
|
||
|
||
return new RequestManagementDtoRs(
|
||
user.sub,
|
||
null,
|
||
currentStepValue,
|
||
nextStepValue,
|
||
"please add plate",
|
||
requestId,
|
||
);
|
||
} catch (err) {
|
||
return new HttpException(err.message, err.status);
|
||
}
|
||
}
|
||
|
||
async addPlate(
|
||
sandHubReport: any,
|
||
request: any,
|
||
user: any,
|
||
body: AddPlateDto,
|
||
partyType: "firstParty" | "secondParty",
|
||
) {
|
||
const clientName = sandHubReport?.CompanyName;
|
||
const companyCode = sandHubReport?.CompanyCode;
|
||
|
||
const client =
|
||
await this.clientService.findClientWithCompanyCode(+companyCode);
|
||
|
||
if (!client) {
|
||
throw new HttpException("Client not found", HttpStatus.CONFLICT);
|
||
}
|
||
|
||
const partyDetails =
|
||
partyType === "firstParty" ? "firstPartyDetails" : "secondPartyDetails";
|
||
const stepsEnum =
|
||
partyType === "firstParty" ? StepsEnum.F_addPlate : StepsEnum.S_addPlate;
|
||
const currentStep =
|
||
partyType === "firstParty" ? StepsEnum.F_addPlate : StepsEnum.S_addPlate;
|
||
|
||
// Determine next step based on request type
|
||
let nextStep: StepsEnum;
|
||
if (request.type === "CAR_BODY") {
|
||
// For CAR_BODY, after plate always go to location
|
||
// (Form and video were already completed before plate)
|
||
nextStep = StepsEnum.F_addLocation;
|
||
} else {
|
||
// THIRD_PARTY or default flow
|
||
nextStep = partyType === "firstParty"
|
||
? StepsEnum.F_addLocation
|
||
: StepsEnum.S_addLocation;
|
||
}
|
||
|
||
try {
|
||
await this.userDbService.findOneAndUpdate(
|
||
{ _id: user.sub },
|
||
{ clientKey: client["_doc"]._id },
|
||
);
|
||
|
||
const sandHubDocData = {
|
||
...sandHubReport,
|
||
plate: body.plate,
|
||
nationalCode: body.nationalCodeOfInsurer,
|
||
};
|
||
const sandHubDoc = await this.sandHubService.sandHubDocumentCreator(
|
||
user.sub,
|
||
request["_doc"]._id,
|
||
sandHubDocData,
|
||
);
|
||
|
||
if (partyType === "firstParty") {
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: request._id },
|
||
{
|
||
sanHubId: sandHubDoc["_doc"]._id,
|
||
},
|
||
);
|
||
}
|
||
|
||
// Build $set object with all field updates
|
||
const setFields: any = {
|
||
currentStep: currentStep,
|
||
nextStep: nextStep,
|
||
[`${partyDetails}.nationalCodeOfInsurer`]: body.nationalCodeOfInsurer,
|
||
[`${partyDetails}.${partyType}Plate`]: body.plate,
|
||
[`${partyDetails}.${partyType}CarDetail.carName`]:
|
||
sandHubReport.MapTypNam,
|
||
[`${partyDetails}.${partyType}CarDetail.insurerBirthday`]:
|
||
body.insurerBirthday,
|
||
[`${partyDetails}.${partyType}CarDetail.driverBirthday`]:
|
||
body.driverBirthday,
|
||
[`${partyDetails}.${partyType}CarDetail.carType`]: `${sandHubReport.UsageField} / ${sandHubReport.MapUsageName || "-"}`,
|
||
[`${partyDetails}.${partyType}CarDetail.isNewCar`]: body.isNewCar,
|
||
[`${partyDetails}.${partyType}Client.clientId`]: client["_doc"]._id,
|
||
[`${partyDetails}.${partyType}Client.clientName`]: clientName,
|
||
[`${partyDetails}.${partyType}Certificate`]: body.userNoCertificate,
|
||
[`${partyDetails}.${partyType}InsuranceDetail.insuranceId`]:
|
||
sandHubReport.LastCompanyDocumentNumber,
|
||
[`${partyDetails}.${partyType}InsuranceDetail.insuranceCompany`]:
|
||
clientName,
|
||
[`${partyDetails}.${partyType}InsuranceDetail.financialCommitmentCeiling`]:
|
||
sandHubReport.FinancialCvrCptl,
|
||
[`${partyDetails}.${partyType}InsuranceDetail.startDate`]:
|
||
sandHubReport.IssueDate,
|
||
[`${partyDetails}.${partyType}InsuranceDetail.endDate`]:
|
||
sandHubReport.EndDate,
|
||
};
|
||
|
||
// For CAR_BODY type, also fetch and persist mocked car body insurance info
|
||
if (request.type === "CAR_BODY" && partyType === "firstParty") {
|
||
const carBodyInfo = await this.mockCarBodyInsuranceInquiry(
|
||
request._id,
|
||
body.plate,
|
||
body.nationalCodeOfInsurer,
|
||
);
|
||
|
||
this.logger.log(
|
||
`[CAR_BODY] Saving car body insurance data to blame file ${request._id}:`,
|
||
JSON.stringify(carBodyInfo, null, 2),
|
||
);
|
||
|
||
// Add car body insurance fields to $set
|
||
setFields["firstPartyDetails.firstPartyCarBodyInsuranceDetail.policyNumber"] =
|
||
carBodyInfo.policyNumber;
|
||
setFields["firstPartyDetails.firstPartyCarBodyInsuranceDetail.startDate"] =
|
||
carBodyInfo.startDate;
|
||
setFields["firstPartyDetails.firstPartyCarBodyInsuranceDetail.endDate"] =
|
||
carBodyInfo.endDate;
|
||
setFields["firstPartyDetails.firstPartyCarBodyInsuranceDetail.insurerCompany"] =
|
||
carBodyInfo.insurerCompany;
|
||
setFields["firstPartyDetails.firstPartyCarBodyInsuranceDetail.coverages"] =
|
||
carBodyInfo.coverages;
|
||
}
|
||
|
||
// Build final update payload with proper MongoDB operators
|
||
const updatePayload: any = {
|
||
$push: { steps: stepsEnum },
|
||
$set: setFields,
|
||
};
|
||
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: request._id },
|
||
updatePayload,
|
||
);
|
||
} catch (er) {
|
||
this.logger.error(er);
|
||
throw new InternalServerErrorException(
|
||
"Failed to update request with plate details.",
|
||
);
|
||
}
|
||
|
||
const nextStepMessage = `Please add location - clientName is ${clientName}`;
|
||
|
||
return new RequestManagementDtoRs(
|
||
request?.firstPartyDetails?.firstPartyId,
|
||
request?.secondPartyDetails?.secondPartyId || null,
|
||
stepsEnum,
|
||
nextStep,
|
||
nextStepMessage,
|
||
request._id,
|
||
);
|
||
}
|
||
|
||
async addPlateToRequest(user: any, body: AddPlateDto, requestId: string) {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
if (
|
||
(body.nationalCodeOfInsurer === body.nationalCodeOfDriver ||
|
||
body.insurerLicense === body.driverLicense) &&
|
||
body.driverIsInsurer === false
|
||
) {
|
||
throw new BadRequestException(
|
||
"Insurer and Driver should be two different persons in this mode.",
|
||
);
|
||
}
|
||
|
||
// Determine if the current user is the first party
|
||
// Check if nextStep is firstParty-addPlate OR if user is the first party phone number
|
||
const isFirstParty =
|
||
(request.nextStep === StepsEnum.F_addPlate ||
|
||
request.currentStep === StepsEnum.F_addPlate) &&
|
||
request.firstPartyDetails.firstPartyPhoneNumber === user.username;
|
||
|
||
if (!isFirstParty) {
|
||
const firstPartyDetails = request.firstPartyDetails as any;
|
||
if (
|
||
firstPartyDetails?.firstPartyPlate ||
|
||
firstPartyDetails?.nationalCodeOfInsurer
|
||
) {
|
||
const isSameNationalCode =
|
||
firstPartyDetails.nationalCodeOfInsurer ===
|
||
body.nationalCodeOfInsurer;
|
||
|
||
const isSamePlate =
|
||
JSON.stringify(firstPartyDetails.firstPartyPlate) ===
|
||
JSON.stringify(body.plate);
|
||
|
||
if (isSameNationalCode || isSamePlate) {
|
||
throw new ConflictException(
|
||
"The plate and national code for the second party cannot be the same as the first party.",
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Proceed with existing logic if the check passes ---
|
||
// 1) Main third-party/block inquiry (existing behavior)
|
||
const sanHubResponse = await this.sandHubService.getSandHubResponse({
|
||
plate: body.plate,
|
||
nationalCodeOfInsurer: body.nationalCodeOfInsurer,
|
||
});
|
||
|
||
if (sanHubResponse.Error) {
|
||
throw new HttpException(
|
||
sanHubResponse.Error.Message,
|
||
HttpStatus.BAD_REQUEST,
|
||
);
|
||
}
|
||
|
||
// 2) Personal inquiry (new shared SandHub API)
|
||
// This is a best-effort call; if it fails we log and continue.
|
||
try {
|
||
const personalInquiry = await this.sandHubService.getPersonalInquiry(
|
||
body.nationalCodeOfInsurer,
|
||
body.insurerBirthday,
|
||
);
|
||
this.logger.log(
|
||
`[SANDHUB] Personal inquiry succeeded for nationalCode=${body.nationalCodeOfInsurer}: ${JSON.stringify(
|
||
personalInquiry,
|
||
)}`,
|
||
);
|
||
// NOTE: We are not persisting this data yet; once the exact fields
|
||
// are finalized, we can map and store them on the request document.
|
||
} catch (err) {
|
||
this.logger.error(
|
||
`[SANDHUB] Personal inquiry failed for nationalCode=${body.nationalCodeOfInsurer}: ${err?.message || err}`,
|
||
);
|
||
}
|
||
|
||
return await this.addPlate(
|
||
sanHubResponse,
|
||
request,
|
||
user,
|
||
body,
|
||
isFirstParty ? "firstParty" : "secondParty",
|
||
);
|
||
}
|
||
|
||
async carBodyFormStep(
|
||
reqBody: CarBodyFormDto,
|
||
user,
|
||
requestId,
|
||
): Promise<RequestManagementDtoRs> {
|
||
const foundRequest =
|
||
await this.requestManagementDbService.findOne(requestId);
|
||
|
||
if (!foundRequest) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// Validate that this is a CAR_BODY type request
|
||
if (foundRequest?.type !== "CAR_BODY") {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for CAR_BODY type requests",
|
||
);
|
||
}
|
||
|
||
// Validate that the user is the first party (or expert filling their own file)
|
||
if (
|
||
!foundRequest.expertInitiated &&
|
||
foundRequest?.firstPartyDetails?.firstPartyPhoneNumber !== user.username
|
||
) {
|
||
throw new ForbiddenException(
|
||
"Only the first party can complete this step",
|
||
);
|
||
}
|
||
|
||
// For CAR_BODY flow: after form, always go to video upload
|
||
// We'll assume guilty by default for car accidents (no second form needed)
|
||
const nextStep = StepsEnum.CarBodyVideo;
|
||
|
||
const createRequestFile =
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
blameStatus: ReqBlameStatus.PendingForFirstParty,
|
||
carBodyForm: {
|
||
firstPartyId: user.sub,
|
||
firstPartyPhoneNumber: user.username,
|
||
carBodyForm: reqBody,
|
||
// Assume guilty by default for car accidents
|
||
isGuilty: reqBody.car ? true : false,
|
||
},
|
||
currentStep: StepsEnum.CarBodyForm,
|
||
nextStep: nextStep,
|
||
$push: { steps: StepsEnum.CarBodyForm },
|
||
},
|
||
);
|
||
|
||
return new RequestManagementDtoRs(
|
||
createRequestFile.firstPartyDetails.firstPartyId,
|
||
null,
|
||
StepsEnum.CarBodyForm,
|
||
nextStep,
|
||
"Please upload accident video",
|
||
createRequestFile._id,
|
||
);
|
||
}
|
||
|
||
async carBodySecondForm(
|
||
reqBody: CarBodySecondForm,
|
||
user,
|
||
requestId,
|
||
): Promise<RequestManagementDtoRs> {
|
||
// This endpoint is deprecated - second form is no longer needed
|
||
// We now assume guilty by default in the first form
|
||
throw new BadRequestException(
|
||
"This endpoint is deprecated. The second form is no longer required. Users are assumed guilty by default for car accidents.",
|
||
);
|
||
}
|
||
|
||
async addDetailToRequestLocation(
|
||
userId: string,
|
||
body: LocationDto,
|
||
requestId,
|
||
user?: any, // Optional user object for expert verification
|
||
) {
|
||
const findRequest =
|
||
await this.requestManagementDbService.findOne(requestId);
|
||
if (!findRequest) throw new NotFoundException("requestId not found");
|
||
|
||
// Allow first party only
|
||
if (findRequest.firstPartyDetails.firstPartyId == new Types.ObjectId(userId)) {
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
firstPartyLocation: body,
|
||
currentStep: StepsEnum.F_addLocation,
|
||
nextStep: StepsEnum.F_addVoice,
|
||
$push: { steps: StepsEnum.F_addLocation }, // Ensure step is saved
|
||
},
|
||
);
|
||
|
||
return new RequestManagementDtoRs(
|
||
findRequest.firstPartyDetails.firstPartyId,
|
||
findRequest?.secondPartyDetails?.secondPartyId || null,
|
||
StepsEnum.F_addLocation,
|
||
StepsEnum.F_addVoice,
|
||
"please add detail",
|
||
requestId,
|
||
);
|
||
}
|
||
if (
|
||
findRequest?.secondPartyDetails?.secondPartyId ==
|
||
new Types.ObjectId(userId)
|
||
) {
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
secondPartyLocation: body,
|
||
currentStep: StepsEnum.S_addLocation,
|
||
nextStep: StepsEnum.S_addVoice,
|
||
$push: { steps: StepsEnum.S_addLocation }, // Ensure step is saved
|
||
},
|
||
);
|
||
return new RequestManagementDtoRs(
|
||
findRequest.firstPartyDetails.firstPartyId,
|
||
findRequest?.secondPartyDetails?.secondPartyId || null,
|
||
StepsEnum.S_addLocation,
|
||
StepsEnum.S_addVoice,
|
||
"please add detail",
|
||
requestId,
|
||
);
|
||
} else {
|
||
throw new BadGatewayException("parties not allowed");
|
||
}
|
||
}
|
||
|
||
async voiceUploadStep(userId: any, voice: Express.Multer.File, requestId) {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("request is not found");
|
||
}
|
||
|
||
const voiceDoc = await this.blameVoiceDbService.create({
|
||
fileName: voice.filename,
|
||
path: voice.path,
|
||
requestId,
|
||
});
|
||
|
||
// Allow first party
|
||
if (request.firstPartyDetails.firstPartyId === userId.sub) {
|
||
try {
|
||
const updated = await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
currentStep: StepsEnum.F_addVoice,
|
||
nextStep: StepsEnum.F_addDescription,
|
||
$push: {
|
||
"firstPartyDetails.firstPartyFile.voices": voiceDoc["_id"],
|
||
steps: StepsEnum.F_addVoice,
|
||
},
|
||
},
|
||
);
|
||
if (updated == undefined) {
|
||
return new BadGatewayException("update failed");
|
||
}
|
||
return new RequestManagementDtoRs(
|
||
request.firstPartyDetails.firstPartyId,
|
||
request?.secondPartyDetails?.secondPartyId || null,
|
||
StepsEnum.F_addVoice,
|
||
StepsEnum.F_addDescription,
|
||
"please add description",
|
||
requestId,
|
||
);
|
||
} catch (err) {
|
||
console.log(err);
|
||
throw new ExternalExceptionFilter();
|
||
}
|
||
}
|
||
if (request?.secondPartyDetails?.secondPartyId === userId.sub) {
|
||
try {
|
||
const updated = await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
currentStep: StepsEnum.S_addVoice,
|
||
nextStep: StepsEnum.S_addDescription,
|
||
$push: {
|
||
"secondPartyDetails.secondPartyFiles.voices": voiceDoc["_id"],
|
||
steps: StepsEnum.S_addVoice,
|
||
},
|
||
},
|
||
);
|
||
if (updated == undefined) {
|
||
return new BadGatewayException("update failed");
|
||
}
|
||
return new RequestManagementDtoRs(
|
||
request.firstPartyDetails.firstPartyId,
|
||
request?.secondPartyDetails?.secondPartyId || null,
|
||
StepsEnum.S_addVoice,
|
||
StepsEnum.S_addDescription,
|
||
"add Description for SecondParty",
|
||
requestId,
|
||
);
|
||
} catch (err) {
|
||
console.log(err);
|
||
throw new ExternalExceptionFilter();
|
||
}
|
||
}
|
||
}
|
||
|
||
async addDescriptionToRequest(
|
||
user: any,
|
||
body: DescriptionDto,
|
||
requestId: string,
|
||
) {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// --- First Party Flow ---
|
||
if (request.firstPartyDetails.firstPartyId == user.sub) {
|
||
// Build base update payload with proper MongoDB operators
|
||
const nextStepValue =
|
||
request.type === "THIRD_PARTY"
|
||
? StepsEnum.F_addSecondPartyPhoneNumber
|
||
: StepsEnum.AddSignatures;
|
||
const blameStatusValue =
|
||
request.type === "THIRD_PARTY"
|
||
? ReqBlameStatus.PendingForFirstParty
|
||
: ReqBlameStatus.WaitingForSignatures;
|
||
|
||
// Ensure firstPartyFile exists and initialize descriptions array if needed
|
||
const updatePayload: any = {
|
||
$set: {
|
||
currentStep: StepsEnum.F_addDescription,
|
||
nextStep: nextStepValue,
|
||
blameStatus: blameStatusValue,
|
||
},
|
||
$push: {
|
||
steps: StepsEnum.F_addDescription,
|
||
},
|
||
};
|
||
|
||
// Initialize firstPartyFile.descriptions if it doesn't exist, then push description
|
||
if (!request.firstPartyDetails.firstPartyFile || !request.firstPartyDetails.firstPartyFile.descriptions) {
|
||
// Initialize the descriptions array if it doesn't exist
|
||
if (!request.firstPartyDetails.firstPartyFile) {
|
||
updatePayload.$set["firstPartyDetails.firstPartyFile"] = {};
|
||
}
|
||
updatePayload.$set["firstPartyDetails.firstPartyFile.descriptions"] = [body.desc];
|
||
} else {
|
||
// If descriptions array exists, push to it
|
||
updatePayload.$push["firstPartyDetails.firstPartyFile.descriptions"] = body.desc;
|
||
}
|
||
|
||
// Add CAR_BODY specific fields if type is CAR_BODY
|
||
if (request.type === "CAR_BODY") {
|
||
if (body.accidentDate) {
|
||
updatePayload.$set["firstPartyDetails.firstPartyFile.accidentDate"] = body.accidentDate;
|
||
}
|
||
if (body.accidentTime) {
|
||
updatePayload.$set["firstPartyDetails.firstPartyFile.accidentTime"] = body.accidentTime;
|
||
}
|
||
if (body.weatherCondition) {
|
||
updatePayload.$set["firstPartyDetails.firstPartyFile.weatherCondition"] = body.weatherCondition;
|
||
}
|
||
if (body.roadCondition) {
|
||
updatePayload.$set["firstPartyDetails.firstPartyFile.roadCondition"] = body.roadCondition;
|
||
}
|
||
if (body.lightCondition) {
|
||
updatePayload.$set["firstPartyDetails.firstPartyFile.lightCondition"] = body.lightCondition;
|
||
}
|
||
}
|
||
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
updatePayload,
|
||
);
|
||
|
||
// For expert-initiated files, ensure auto-assignment and add history
|
||
if (request.expertInitiated) {
|
||
await this.ensureExpertAssignment(requestId);
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"FIRST_PARTY_DESCRIPTION_COMPLETED",
|
||
new Types.ObjectId(user.sub),
|
||
undefined,
|
||
request.filledBy === FilledBy.EXPERT ? "expert" : "customer",
|
||
);
|
||
}
|
||
|
||
const nextStepMessage = request.type === "CAR_BODY"
|
||
? "Please sign the document to complete your request"
|
||
: "add secondParty to request";
|
||
|
||
return new RequestManagementDtoRs(
|
||
request.firstPartyDetails.firstPartyId,
|
||
request?.secondPartyDetails?.secondPartyId || null,
|
||
StepsEnum.F_addDescription,
|
||
nextStepValue,
|
||
nextStepMessage,
|
||
requestId,
|
||
);
|
||
}
|
||
|
||
// --- Second Party Flow ---
|
||
if (request?.secondPartyDetails?.secondPartyId == user.sub) {
|
||
const firstPartyForm = request.firstPartyDetails.firstPartyInitialForm;
|
||
const secondPartyForm = request.secondPartyDetails.secondPartyInitialForm;
|
||
|
||
const shouldAutoResolve =
|
||
(firstPartyForm?.imDamaged && secondPartyForm?.imGuilty) ||
|
||
(secondPartyForm?.imDamaged && firstPartyForm?.imGuilty);
|
||
|
||
let updatePayload: any;
|
||
|
||
if (shouldAutoResolve) {
|
||
// Build the payload for the auto-resolve (agreement) flow
|
||
const guiltyPartyId = firstPartyForm.imGuilty
|
||
? request.firstPartyDetails.firstPartyId
|
||
: request.secondPartyDetails.secondPartyId;
|
||
|
||
updatePayload = {
|
||
$set: {
|
||
blameStatus: ReqBlameStatus.WaitingForSignatures,
|
||
currentStep: StepsEnum.S_addDescription,
|
||
nextStep: StepsEnum.AddSignatures,
|
||
expertSubmitReply: {
|
||
description: "منتظر امضای طرفین برای تایید توافق.",
|
||
guiltyUserId: guiltyPartyId,
|
||
submitTime: new Date(),
|
||
fields: { /* TODO: The accidentReason field is mocked and is default option for when the shouldAutoResolve is true */
|
||
accidentReason: { /* it should be removed and replaced with a proper mechanism to identify the accident reason */
|
||
id: 2,
|
||
label: "تغيير مسير ناگهانی",
|
||
fanavaran: 4,
|
||
}
|
||
}
|
||
},
|
||
},
|
||
$push: {
|
||
steps: StepsEnum.S_addDescription,
|
||
},
|
||
};
|
||
|
||
// Initialize secondPartyFiles.descriptions if it doesn't exist, then push description
|
||
if (!request.secondPartyDetails?.secondPartyFiles || !request.secondPartyDetails.secondPartyFiles.descriptions) {
|
||
// Initialize the descriptions array if it doesn't exist
|
||
if (!request.secondPartyDetails.secondPartyFiles) {
|
||
updatePayload.$set["secondPartyDetails.secondPartyFiles"] = {};
|
||
}
|
||
updatePayload.$set["secondPartyDetails.secondPartyFiles.descriptions"] = [body.desc];
|
||
} else {
|
||
// If descriptions array exists, push to it
|
||
updatePayload.$push["secondPartyDetails.secondPartyFiles.descriptions"] = body.desc;
|
||
}
|
||
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
updatePayload,
|
||
);
|
||
|
||
return new RequestManagementDtoRs(
|
||
request.firstPartyDetails.firstPartyId,
|
||
user.sub,
|
||
StepsEnum.S_addDescription,
|
||
ReqBlameStatus.WaitingForSignatures,
|
||
"Agreement detected. Awaiting signatures from both parties.",
|
||
requestId,
|
||
);
|
||
} else {
|
||
// Build the payload for the standard (disagreement) flow
|
||
updatePayload = {
|
||
$set: {
|
||
blameStatus: ReqBlameStatus.UnChecked,
|
||
currentStep: StepsEnum.S_addDescription,
|
||
nextStep: StepsEnum.S_completed,
|
||
},
|
||
$push: {
|
||
steps: StepsEnum.S_addDescription,
|
||
},
|
||
};
|
||
|
||
// Initialize secondPartyFiles.descriptions if it doesn't exist, then push description
|
||
if (!request.secondPartyDetails?.secondPartyFiles || !request.secondPartyDetails.secondPartyFiles.descriptions) {
|
||
// Initialize the descriptions array if it doesn't exist
|
||
if (!request.secondPartyDetails.secondPartyFiles) {
|
||
updatePayload.$set["secondPartyDetails.secondPartyFiles"] = {};
|
||
}
|
||
updatePayload.$set["secondPartyDetails.secondPartyFiles.descriptions"] = [body.desc];
|
||
} else {
|
||
// If descriptions array exists, push to it
|
||
updatePayload.$push["secondPartyDetails.secondPartyFiles.descriptions"] = body.desc;
|
||
}
|
||
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
updatePayload,
|
||
);
|
||
|
||
// For expert-initiated files, ensure auto-assignment and add history
|
||
if (request.expertInitiated) {
|
||
await this.ensureExpertAssignment(requestId);
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"SECOND_PARTY_DESCRIPTION_COMPLETED",
|
||
new Types.ObjectId(user.sub),
|
||
undefined,
|
||
request.filledBy === FilledBy.EXPERT ? "expert" : "customer",
|
||
);
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"READY_FOR_EXPERT_EVALUATION",
|
||
request.initiatedBy,
|
||
undefined,
|
||
"system",
|
||
);
|
||
}
|
||
|
||
return new RequestManagementDtoRs(
|
||
request.firstPartyDetails.firstPartyId,
|
||
user.sub,
|
||
StepsEnum.S_addSecondPartyPhoneNumber,
|
||
"expertOpinion",
|
||
"please wait... for expert reply",
|
||
requestId,
|
||
);
|
||
}
|
||
}
|
||
|
||
throw new ForbiddenException("User is not a party to this request.");
|
||
}
|
||
|
||
async addSecondPartyDetailsToRequest(
|
||
phoneNumber,
|
||
requestId,
|
||
firstPartyUser,
|
||
frontendRoutes,
|
||
req,
|
||
) {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
if (request?.secondPartyDetails?.secondPartyId) {
|
||
throw new BadGatewayException("Second party has already been added");
|
||
}
|
||
if (request.firstPartyDetails.firstPartyPhoneNumber === phoneNumber) {
|
||
throw new BadRequestException(
|
||
"Second party phone number cannot be the same as the first party",
|
||
);
|
||
}
|
||
|
||
// Only perform the update if the second party details don't exist yet.
|
||
if (!request.secondPartyDetails) {
|
||
const updatePayload = {
|
||
blameStatus: ReqBlameStatus.PendingForSecondParty,
|
||
"secondPartyDetails.secondPartyPhoneNumber": phoneNumber,
|
||
currentStep: StepsEnum.F_completed,
|
||
nextStep: StepsEnum.S_InitialForm,
|
||
$push: {
|
||
steps: StepsEnum.F_addSecondPartyPhoneNumber,
|
||
},
|
||
};
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
updatePayload,
|
||
);
|
||
}
|
||
|
||
// Send the SMS after the database update is complete.
|
||
const URL = `${process.env.URL}/${frontendRoutes}?token=${requestId}`;
|
||
try {
|
||
const smsRes = await this.smsManagerService.verifyLookUp({
|
||
token: URL,
|
||
template: "yara724-invite-link",
|
||
receptor: phoneNumber,
|
||
});
|
||
this.logger.log(smsRes);
|
||
} catch (er) {
|
||
this.logger.error("SMS sending failed:", er);
|
||
}
|
||
|
||
return { url: URL };
|
||
}
|
||
|
||
async myRequests(phoneNumber) {
|
||
const res = { listOfFirstParty: [], listOfSecondParty: [] };
|
||
const blameStatuses = [
|
||
ReqBlameStatus.CheckedRequest,
|
||
ReqBlameStatus.UnChecked,
|
||
ReqBlameStatus.PendingForFirstParty,
|
||
ReqBlameStatus.PendingForSecondParty,
|
||
ReqBlameStatus.ReviewRequest,
|
||
ReqBlameStatus.CheckAgain,
|
||
ReqBlameStatus.UserPending,
|
||
ReqBlameStatus.CloseRequest,
|
||
ReqBlameStatus.WaitForUserAccept,
|
||
ReqBlameStatus.WaitingForSignatures,
|
||
ReqBlameStatus.InPersonVisit,
|
||
];
|
||
|
||
const parties = await this.requestManagementDbService.findAll({
|
||
$or: [
|
||
{
|
||
$or: [
|
||
{ "firstPartyDetails.firstPartyPhoneNumber": phoneNumber },
|
||
{ "secondPartyDetails.secondPartyPhoneNumber": phoneNumber },
|
||
],
|
||
blameStatus: { $in: blameStatuses },
|
||
},
|
||
],
|
||
});
|
||
// TODO create a DTO for Response
|
||
parties.forEach((p) => {
|
||
if (p?.secondPartyDetails?.secondPartyPhoneNumber == phoneNumber) {
|
||
res.listOfSecondParty.push({
|
||
id: p.id,
|
||
blameStatus: p.blameStatus,
|
||
requestNumber: p.requestNumber,
|
||
createdAt: new Date(p.createdAt)
|
||
.toLocaleString("fa-IR")
|
||
.split("،")[0],
|
||
timeCreatedAt: new Date(p.createdAt)
|
||
.toLocaleTimeString("fa-IR")
|
||
.split(",")[0],
|
||
type: p.type || "THIRD_PARTY", // Include type field
|
||
});
|
||
}
|
||
|
||
if (p.firstPartyDetails.firstPartyPhoneNumber == phoneNumber) {
|
||
console.log(
|
||
new Date(p.createdAt).toLocaleTimeString("fa-IR").split(","),
|
||
);
|
||
res.listOfFirstParty.push({
|
||
id: p.id,
|
||
blameStatus: p.blameStatus,
|
||
requestNumber: p.requestNumber,
|
||
createdAt: new Date(p.createdAt)
|
||
.toLocaleString("fa-IR")
|
||
.split("،")[0],
|
||
timeCreatedAt: new Date(p.createdAt)
|
||
.toLocaleTimeString("fa-IR")
|
||
.split(",")[0],
|
||
type: p.type || "THIRD_PARTY", // Include type field
|
||
});
|
||
}
|
||
});
|
||
return res;
|
||
}
|
||
|
||
async getCreatableClaims(user: any) {
|
||
const userId = user.sub;
|
||
|
||
const creatableBlameFiles = await this.requestManagementDbService.findAll({
|
||
blameStatus: ReqBlameStatus.CloseRequest,
|
||
|
||
$or: [
|
||
{ "firstPartyDetails.firstPartyId": userId },
|
||
{ "secondPartyDetails.secondPartyId": userId },
|
||
],
|
||
|
||
"expertSubmitReply.guiltyUserId": { $ne: userId },
|
||
});
|
||
|
||
if (!creatableBlameFiles || creatableBlameFiles.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
// Fetch existing claim files for all blame files in parallel
|
||
const claimFilesPromises = creatableBlameFiles.map((blameFile) =>
|
||
this.claimRequestManagementDbService.findOne(
|
||
undefined,
|
||
{ "blameFile._id": new Types.ObjectId(blameFile._id) },
|
||
),
|
||
);
|
||
const existingClaimFiles = await Promise.all(claimFilesPromises);
|
||
|
||
// Create a map of blame file ID to claim file ID for quick lookup
|
||
const blameToClaimMap = new Map();
|
||
creatableBlameFiles.forEach((blameFile, index) => {
|
||
const claimFile = existingClaimFiles[index];
|
||
if (claimFile) {
|
||
blameToClaimMap.set(blameFile._id.toString(), claimFile._id.toString());
|
||
}
|
||
});
|
||
|
||
const formattedResponse = creatableBlameFiles.map((p) => {
|
||
const claimId = blameToClaimMap.get(p._id.toString());
|
||
return {
|
||
id: p.id,
|
||
blameStatus: p.blameStatus,
|
||
requestNumber: p.requestNumber,
|
||
createdAt: new Date(p.createdAt).toLocaleString("fa-IR").split("،")[0],
|
||
timeCreatedAt: new Date(p.createdAt)
|
||
.toLocaleTimeString("fa-IR")
|
||
.split(",")[0],
|
||
...(claimId && { claimId }),
|
||
};
|
||
});
|
||
|
||
return formattedResponse;
|
||
}
|
||
|
||
async requestDetailsWithoutStatus(
|
||
request: RequestManagementModel,
|
||
user: any,
|
||
) {
|
||
const parties = this.determineUserPartyRole(request, user);
|
||
|
||
request.actorsChecker =
|
||
request.actorsChecker[request.actorsChecker.length - 1];
|
||
|
||
return { theParties: parties, requestDetail: request };
|
||
}
|
||
|
||
async requestDetailsCheckedStatus(
|
||
request: any,
|
||
user: any,
|
||
): Promise<DetailsReplyDtoRs> {
|
||
const finalReply =
|
||
request.expertSubmitReplyFinal || request.expertSubmitReply;
|
||
|
||
if (!finalReply) {
|
||
throw new NotFoundException("Expert reply not found for this request.");
|
||
}
|
||
|
||
const { guiltyUserId } = finalReply;
|
||
const isGuilty = guiltyUserId == user.sub;
|
||
|
||
let actorDetail;
|
||
for (const a of request.actorsChecker) {
|
||
if (Object.keys(a)[0].toString() === ReqBlameStatus.CheckedRequest) {
|
||
actorDetail = a;
|
||
}
|
||
}
|
||
|
||
let steps: ReqBlameStatus;
|
||
if (!finalReply.firstPartyComment) {
|
||
steps = ReqBlameStatus.PendingForSecondParty;
|
||
} else if (!finalReply.secondPartyComment) {
|
||
steps = ReqBlameStatus.PendingForFirstParty;
|
||
} else {
|
||
steps = ReqBlameStatus.WaitForUserAccept;
|
||
}
|
||
|
||
await this.requestManagementDbService.findByIdAndUpdate(request._id, {
|
||
currentStep: steps,
|
||
$push: { steps: steps },
|
||
});
|
||
|
||
const theParties = this.determineUserPartyRole(request, user);
|
||
|
||
return new DetailsReplyDtoRs(request, {
|
||
actorDetail,
|
||
isGuilty,
|
||
steps,
|
||
theParties,
|
||
});
|
||
}
|
||
|
||
async requestDetailCloseStatus(request: RequestManagementModel) {
|
||
const finalReply =
|
||
request.expertSubmitReplyFinal || request.expertSubmitReply;
|
||
|
||
if (!finalReply) {
|
||
return { message: "اطلاعات کارشناسی برای این پرونده یافت نشد." };
|
||
}
|
||
|
||
const authoritativeReply = finalReply as any;
|
||
|
||
const bothPartiesAccepted =
|
||
authoritativeReply.firstPartyComment?.isAccept &&
|
||
authoritativeReply.secondPartyComment?.isAccept;
|
||
|
||
const wasAutoClosed =
|
||
request.autoCloseTriggerTime &&
|
||
new Date(request.autoCloseTriggerTime) < new Date();
|
||
|
||
if (bothPartiesAccepted || wasAutoClosed) {
|
||
const guiltyUserId = authoritativeReply.guiltyUserId;
|
||
if (!guiltyUserId) {
|
||
return { message: "اطلاعات طرف مقصر در پرونده یافت نشد." };
|
||
}
|
||
|
||
const { firstPartyDetails, secondPartyDetails } = request;
|
||
|
||
const isFirstPartyGuilty =
|
||
firstPartyDetails.firstPartyId.toString() === guiltyUserId.toString();
|
||
|
||
const guiltyUserDetail = isFirstPartyGuilty
|
||
? [firstPartyDetails]
|
||
: [secondPartyDetails];
|
||
const damageUserDetail = isFirstPartyGuilty
|
||
? [secondPartyDetails]
|
||
: [firstPartyDetails];
|
||
|
||
return {
|
||
requestId: request["_id"],
|
||
blameStatus: request.blameStatus,
|
||
damageUserDetail,
|
||
guiltyUserDetail,
|
||
expertResendReply: request.expertResendReply,
|
||
expertSubmitReply: request.expertSubmitReply,
|
||
expertSubmitReplyFinal: request.expertSubmitReplyFinal,
|
||
};
|
||
} else {
|
||
return { message: "یکی از طرفین پرونده را تایید نکرده است." };
|
||
}
|
||
}
|
||
|
||
async requestDetailsUserPendingStatus(
|
||
request: RequestManagementModel,
|
||
user: any,
|
||
) {
|
||
const { expertResendReply } = request;
|
||
|
||
const processPartyDetails = (partyKey: "firstParty" | "secondParty") => {
|
||
const partyReply = expertResendReply?.[partyKey];
|
||
|
||
if (!partyReply || user.sub !== partyReply[`${partyKey}Id`]) {
|
||
return null;
|
||
}
|
||
|
||
const descriptionString = partyReply[`${partyKey}Description`] || "";
|
||
const allParts = descriptionString
|
||
.split(",")
|
||
.map((p) => p.trim())
|
||
.filter(Boolean);
|
||
|
||
const validDocTypes = Object.values(BlameDocumentType);
|
||
const requiredDocuments: BlameDocumentType[] = [];
|
||
const descriptionTextParts: string[] = [];
|
||
|
||
for (const part of allParts) {
|
||
const matchingDocType = validDocTypes.find(
|
||
(docType) =>
|
||
docType.replace(/_/g, "").toUpperCase() === part.toUpperCase(),
|
||
);
|
||
|
||
if (matchingDocType) {
|
||
requiredDocuments.push(matchingDocType);
|
||
} else {
|
||
descriptionTextParts.push(part);
|
||
}
|
||
}
|
||
|
||
const description = descriptionTextParts.join(", ");
|
||
|
||
const otherPartyKey =
|
||
partyKey === "firstParty" ? "secondParty" : "firstParty";
|
||
const otherPartyHasReplied =
|
||
!!expertResendReply?.[otherPartyKey]?.userReply;
|
||
|
||
const nextStep = otherPartyHasReplied
|
||
? "Wait For Expert Comment"
|
||
: partyKey === "firstParty"
|
||
? "Wait For Second Party Reply"
|
||
: "Wait For First Party Reply";
|
||
|
||
return {
|
||
userId: partyReply[`${partyKey}Id`],
|
||
requiredDocuments,
|
||
description,
|
||
hasReplied: partyReply.userReply != null,
|
||
hasVoice: partyReply.voice != null,
|
||
nextStep: nextStep,
|
||
};
|
||
};
|
||
|
||
const details =
|
||
processPartyDetails("firstParty") || processPartyDetails("secondParty");
|
||
|
||
return details;
|
||
}
|
||
|
||
// TODO fix requestDetailsCheckAginStatus again
|
||
async requestDetailsCheckAgainStatus(request, user) {
|
||
const { expertResendReply } = request;
|
||
const detail = [];
|
||
|
||
const addDetail = (
|
||
party,
|
||
partyId,
|
||
partyDescription,
|
||
userReply,
|
||
voice,
|
||
step1,
|
||
step2,
|
||
) => {
|
||
if (user.sub === expertResendReply?.[party]?.[`${party}Id`]) {
|
||
detail.push(expertResendReply?.[party][`${party}Id`], {
|
||
description: expertResendReply?.[party][`${party}Description`],
|
||
reply: expertResendReply[party]?.userReply !== null,
|
||
voice: !!expertResendReply[party]?.voice,
|
||
});
|
||
if (
|
||
expertResendReply?.[party][`${party}Id`] &&
|
||
!expertResendReply?.[
|
||
party === "firstParty" ? "secondParty" : "firstParty"
|
||
]?.userReply
|
||
) {
|
||
detail.push({ step: step1 });
|
||
} else {
|
||
detail.push({ step: step2 });
|
||
}
|
||
}
|
||
};
|
||
|
||
addDetail(
|
||
"firstParty",
|
||
"firstPartyId",
|
||
"firstPartyDescription",
|
||
"userReply",
|
||
"voice",
|
||
"Wait For Second Party Reply",
|
||
"Wait For Expert Comment",
|
||
);
|
||
addDetail(
|
||
"secondParty",
|
||
"secondPartyId",
|
||
"secondPartyDescription",
|
||
"userReply",
|
||
"voice",
|
||
"Wait For First Party Reply",
|
||
"Wait For Expert Comment",
|
||
);
|
||
|
||
return detail;
|
||
}
|
||
|
||
// TODO CREATE DTO FOR RESPONSE ONLY
|
||
async requestDetails(user, requestId) {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) throw new NotFoundException("request not found");
|
||
|
||
const formattedRequest = {
|
||
...request,
|
||
createdAt: request.createdAt,
|
||
updatedAt: request.updatedAt,
|
||
};
|
||
|
||
const formattingOptions: Intl.DateTimeFormatOptions = {
|
||
timeZone: "Asia/Tehran",
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
};
|
||
|
||
// Check if createdAt exists and then format it on our new object.
|
||
if (request.createdAt) {
|
||
formattedRequest.createdAt = new Date(request.createdAt).toLocaleString(
|
||
"fa-IR",
|
||
formattingOptions,
|
||
);
|
||
}
|
||
|
||
// Do the same for updatedAt.
|
||
if (request.updatedAt) {
|
||
formattedRequest.updatedAt = new Date(request.updatedAt).toLocaleString(
|
||
"fa-IR",
|
||
formattingOptions,
|
||
) as unknown as Date;
|
||
}
|
||
|
||
switch (request.blameStatus) {
|
||
case ReqBlameStatus.CheckedRequest:
|
||
return await this.requestDetailsCheckedStatus(request, user);
|
||
case ReqBlameStatus.UserPending:
|
||
return await this.requestDetailsUserPendingStatus(request, user);
|
||
case ReqBlameStatus.CheckAgain:
|
||
return await this.requestDetailsCheckAgainStatus(request, user);
|
||
case ReqBlameStatus.CloseRequest:
|
||
return await this.requestDetailCloseStatus(request);
|
||
default:
|
||
// Pass the full user object, not just the username
|
||
return await this.requestDetailsWithoutStatus(request, user);
|
||
}
|
||
}
|
||
|
||
private determineUserPartyRole(
|
||
request: RequestManagementModel,
|
||
user: any,
|
||
): "firstParty" | "secondParty" | null {
|
||
const { firstPartyDetails, secondPartyDetails } = request;
|
||
const userId = user.sub; // The user's unique ID
|
||
const username = user.username; // The user's phone number
|
||
|
||
// Check against both ID and phone number for robustness
|
||
if (
|
||
userId === firstPartyDetails?.firstPartyId?.toString() ||
|
||
username === firstPartyDetails?.firstPartyPhoneNumber
|
||
) {
|
||
return "firstParty";
|
||
}
|
||
if (
|
||
userId === secondPartyDetails?.secondPartyId?.toString() ||
|
||
username === secondPartyDetails?.secondPartyPhoneNumber
|
||
) {
|
||
return "secondParty";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Validate that both expertId and requestId are valid ObjectId strings
|
||
if (!Types.ObjectId.isValid(expertId)) {
|
||
console.warn("Invalid ObjectId format for expertId:", expertId);
|
||
return;
|
||
}
|
||
|
||
if (!Types.ObjectId.isValid(requestId)) {
|
||
console.warn("Invalid ObjectId format for requestId:", requestId);
|
||
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);
|
||
}
|
||
}
|
||
|
||
private async handleDisagreement(
|
||
request: any,
|
||
originalReq: any,
|
||
isFirstPartyTheDisagreer: boolean,
|
||
): Promise<void> {
|
||
await this.requestManagementDbService.findByIdAndUpdate(request._id, {
|
||
blameStatus: ReqBlameStatus.PartiesDisagree,
|
||
$unset: { autoCloseTriggerTime: "" },
|
||
});
|
||
|
||
this.logger.log(
|
||
`Blame request ${request._id} closed with status 'PartiesDisagree'.`,
|
||
);
|
||
|
||
const firstPartyComment = request.expertSubmitReply?.firstPartyComment;
|
||
const secondPartyComment = request.expertSubmitReply?.secondPartyComment;
|
||
|
||
let phoneNumberToNotify: string | null = null;
|
||
if (isFirstPartyTheDisagreer && secondPartyComment?.isAccept === true) {
|
||
phoneNumberToNotify =
|
||
originalReq.secondPartyDetails?.secondPartyPhoneNumber;
|
||
} else if (
|
||
!isFirstPartyTheDisagreer &&
|
||
firstPartyComment?.isAccept === true
|
||
) {
|
||
phoneNumberToNotify =
|
||
originalReq.firstPartyDetails?.firstPartyPhoneNumber;
|
||
}
|
||
|
||
// TODO SMS Sending
|
||
if (phoneNumberToNotify) {
|
||
const message = `متاسفانه طرف مقابل با نظر کارشناس مخالفت کرد. فرآیند آنلاین این پرونده بسته شده است و جهت پیگیری حضوری اقدام نمایید.`;
|
||
await this.smsManagerService.sendMessage({
|
||
receptor: phoneNumberToNotify,
|
||
message,
|
||
});
|
||
}
|
||
}
|
||
|
||
private async handlePostReplyStateChanges(
|
||
updatedReq: any,
|
||
originalReq: any,
|
||
): Promise<void> {
|
||
const isCarBodyType = updatedReq.type === "CAR_BODY";
|
||
|
||
const finalReply = updatedReq.expertResendReply
|
||
? updatedReq.expertSubmitReplyFinal
|
||
: updatedReq.expertSubmitReply;
|
||
|
||
const firstAccept = finalReply?.firstPartyComment?.isAccept;
|
||
const secondAccept = finalReply?.secondPartyComment?.isAccept;
|
||
|
||
// 🟥 Step 1: Handle rejections
|
||
if (!isCarBodyType && (firstAccept === false || secondAccept === false)) {
|
||
await this.handleDisagreement(
|
||
updatedReq,
|
||
originalReq,
|
||
firstAccept === false,
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 🟦 Step 2: CAR_BODY logic (one side only)
|
||
if (isCarBodyType) {
|
||
if (firstAccept) {
|
||
await this.handleFinalStateChange(updatedReq);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 🟩 Step 3: Two-party logic
|
||
if (updatedReq.blameStatus === ReqBlameStatus.WaitingForSignatures) {
|
||
if (firstAccept && secondAccept) {
|
||
await this.finalizeAutoResolvedRequest(updatedReq);
|
||
} else if (firstAccept || secondAccept) {
|
||
const message = `طرف مقابل توافق را امضا کرده است. لطفا جهت نهایی کردن پرونده، امضای خود را ثبت نمایید.`;
|
||
await this.handleOnePartyReplied(
|
||
updatedReq,
|
||
originalReq,
|
||
firstAccept,
|
||
message,
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (firstAccept && secondAccept) {
|
||
await this.handleFinalStateChange(updatedReq);
|
||
} else if (firstAccept || secondAccept) {
|
||
const message = `طرف مقابل نظر کارشناس را قبول و امضا کرده است. در طی 24 ساعت آینده فرصت دارید تا پرونده را تکمیل کرده یا پرونده بسته خواهد شد.`;
|
||
await this.handleOnePartyReplied(
|
||
updatedReq,
|
||
originalReq,
|
||
firstAccept,
|
||
message,
|
||
);
|
||
}
|
||
}
|
||
|
||
private async finalizeAutoResolvedRequest(request: any): Promise<void> {
|
||
await this.requestManagementDbService.findByIdAndUpdate(request._id, {
|
||
blameStatus: ReqBlameStatus.CloseRequest,
|
||
$unset: { autoCloseTriggerTime: "" },
|
||
});
|
||
|
||
this.logger.log(
|
||
`Blame request ${request._id} auto-resolved and closed successfully.`,
|
||
);
|
||
}
|
||
|
||
private buildReplyUpdate(
|
||
request: any,
|
||
userId: string,
|
||
isAccept: boolean,
|
||
signDetail: any,
|
||
): any {
|
||
const isObjection = !!request.expertResendReply;
|
||
const replyField = isObjection
|
||
? "expertSubmitReplyFinal"
|
||
: "expertSubmitReply";
|
||
|
||
// Handle cases where firstPartyId might be undefined (e.g., IN_PERSON expert-filled files)
|
||
const firstPartyId = request.firstPartyDetails?.firstPartyId;
|
||
if (!firstPartyId) {
|
||
throw new BadRequestException(
|
||
"First party ID is missing. Please ensure the file has been properly initialized.",
|
||
);
|
||
}
|
||
|
||
const isFirstParty = userId === firstPartyId.toString();
|
||
|
||
const currentPartyKey = isFirstParty
|
||
? "firstPartyComment"
|
||
: "secondPartyComment";
|
||
const otherPartyKey = isFirstParty
|
||
? "secondPartyComment"
|
||
: "firstPartyComment";
|
||
|
||
const newReplyObject = JSON.parse(
|
||
JSON.stringify(request[replyField] ?? {}),
|
||
);
|
||
|
||
const currentUserComment = {
|
||
isAccept: isAccept,
|
||
signDetail: {
|
||
fileId: signDetail._id,
|
||
fileName: signDetail.fileName,
|
||
},
|
||
};
|
||
|
||
newReplyObject[currentPartyKey] = currentUserComment;
|
||
|
||
if (!newReplyObject[otherPartyKey]) {
|
||
newReplyObject[otherPartyKey] = null;
|
||
}
|
||
|
||
const update = {
|
||
$set: {
|
||
[replyField]: newReplyObject,
|
||
},
|
||
};
|
||
|
||
return update;
|
||
}
|
||
|
||
private async handleFinalStateChange(request: any): Promise<void> {
|
||
const alreadyClosed = request.blameStatus === ReqBlameStatus.CloseRequest;
|
||
const alreadyHandled = request.isHandledStatsUpdated === true;
|
||
|
||
if (!alreadyClosed) {
|
||
await this.requestManagementDbService.findByIdAndUpdate(request._id, {
|
||
blameStatus: ReqBlameStatus.CloseRequest,
|
||
$unset: { autoCloseTriggerTime: "" },
|
||
});
|
||
}
|
||
|
||
if (!alreadyHandled) {
|
||
// Only update expert stats if the request was locked by an expert
|
||
// (CAR_BODY requests might not have actorLocked set)
|
||
if (request.actorLocked?.actorId) {
|
||
// Convert ObjectIds to strings safely
|
||
const actorIdStr = request.actorLocked.actorId.toString ?
|
||
request.actorLocked.actorId.toString() :
|
||
String(request.actorLocked.actorId);
|
||
const requestIdStr = request._id.toString ?
|
||
request._id.toString() :
|
||
String(request._id);
|
||
|
||
await this.updateDamageExpertStats(
|
||
actorIdStr,
|
||
requestIdStr,
|
||
"handled",
|
||
);
|
||
}
|
||
await this.requestManagementDbService.findByIdAndUpdate(request._id, {
|
||
$set: { isHandledStatsUpdated: true },
|
||
});
|
||
}
|
||
}
|
||
|
||
private async handleOnePartyReplied(
|
||
updatedReq: any,
|
||
originalReq: any,
|
||
isFirstPartyAccepted: boolean,
|
||
message: string,
|
||
): Promise<void> {
|
||
if (updatedReq.autoCloseTriggerTime) {
|
||
return;
|
||
}
|
||
|
||
const phoneNumber = isFirstPartyAccepted
|
||
? originalReq.secondPartyDetails?.secondPartyPhoneNumber
|
||
: originalReq.firstPartyDetails?.firstPartyPhoneNumber;
|
||
|
||
if (phoneNumber) {
|
||
// TODO FIX SMS SENDING FOR PARTIES
|
||
await this.smsManagerService.sendMessage({
|
||
receptor: phoneNumber,
|
||
message,
|
||
});
|
||
}
|
||
|
||
await this.autoCloseRequestService.scheduleAutoClose({
|
||
requestId: updatedReq._id.toString(),
|
||
runAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||
});
|
||
}
|
||
|
||
async userReply(
|
||
props,
|
||
isAccept: boolean,
|
||
file: Express.Multer.File,
|
||
): Promise<UserReplyDto> {
|
||
const { requestId, user } = props;
|
||
if (!file) throw new BadRequestException("A signature file is required.");
|
||
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) throw new NotFoundException("Request not found.");
|
||
|
||
const signDetail = await this.userSign.create({
|
||
fileName: file.filename,
|
||
userId: user.sub,
|
||
path: file.path,
|
||
requestId,
|
||
});
|
||
|
||
const updatePayload = this.buildReplyUpdate(
|
||
request,
|
||
user.sub,
|
||
isAccept,
|
||
signDetail,
|
||
);
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
updatePayload,
|
||
);
|
||
|
||
const updatedReq = await this.requestManagementDbService.findOne(requestId);
|
||
|
||
await this.handlePostReplyStateChanges(updatedReq, request);
|
||
|
||
return new UserReplyDto(requestId);
|
||
}
|
||
|
||
async userResend(
|
||
files: { [key: string]: Express.Multer.File[] },
|
||
user: any,
|
||
requestId: string,
|
||
description: string,
|
||
) {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
const partyKey = this.determinePartyAndValidate(request, user.sub);
|
||
|
||
const { voiceId, uploadedDocumentIds } = await this.processResendUploads(
|
||
files,
|
||
requestId,
|
||
);
|
||
|
||
const updatePayload: any = {
|
||
$set: {
|
||
[`expertResendReply.${partyKey}.userReply`]: description,
|
||
},
|
||
};
|
||
|
||
if (voiceId) {
|
||
updatePayload.$set[`expertResendReply.${partyKey}.voice`] = voiceId;
|
||
}
|
||
|
||
for (const docType in uploadedDocumentIds) {
|
||
updatePayload.$set[`expertResendReply.${partyKey}.documents.${docType}`] =
|
||
uploadedDocumentIds[docType];
|
||
}
|
||
|
||
await this.requestManagementDbService.findByIdAndUpdate(
|
||
requestId,
|
||
updatePayload,
|
||
);
|
||
|
||
await this.updateRequestStatusIfNeeded(requestId);
|
||
|
||
return { message: "Update saved successfully." };
|
||
}
|
||
|
||
// Mocked CAR_BODY insurance inquiry – replace with real external API later
|
||
private async mockCarBodyInsuranceInquiry(
|
||
requestId: string,
|
||
plate: any,
|
||
nationalCodeOfInsurer: string,
|
||
): Promise<{
|
||
policyNumber: string;
|
||
startDate: string;
|
||
endDate: string;
|
||
insurerCompany: string;
|
||
coverages: string[];
|
||
}> {
|
||
this.logger.log(
|
||
`[CAR_BODY] Mocking car body insurance inquiry for request ${requestId} (plate=${JSON.stringify(
|
||
plate,
|
||
)}, nationalCodeOfInsurer=${nationalCodeOfInsurer})`,
|
||
);
|
||
|
||
const today = new Date();
|
||
const oneYearLater = new Date(
|
||
today.getFullYear() + 1,
|
||
today.getMonth(),
|
||
today.getDate(),
|
||
);
|
||
|
||
return {
|
||
policyNumber: "CB-MOCK-123456",
|
||
startDate: today.toISOString().slice(0, 10),
|
||
endDate: oneYearLater.toISOString().slice(0, 10),
|
||
insurerCompany: "Mock Car Body Insurance Co.",
|
||
coverages: ["آتشسوزی", "سرقت", "بدنه کامل"],
|
||
};
|
||
}
|
||
|
||
private async processResendUploads(
|
||
files: { [key: string]: Express.Multer.File[] },
|
||
requestId: string,
|
||
) {
|
||
const uploadedDocumentIds: { [key in BlameDocumentType]?: Types.ObjectId } =
|
||
{};
|
||
let voiceId: Types.ObjectId | null = null;
|
||
|
||
for (const fieldName in files) {
|
||
const fileArray = files[fieldName];
|
||
if (fileArray && fileArray.length > 0) {
|
||
const file = fileArray[0];
|
||
|
||
if (fieldName === "voice") {
|
||
voiceId = await this.createVoiceDocument(
|
||
file,
|
||
requestId,
|
||
UploadContext.EXPERT_RESEND,
|
||
);
|
||
} else {
|
||
const documentType = fieldName as BlameDocumentType;
|
||
const docId = await this.createBlameDocument(
|
||
file,
|
||
requestId,
|
||
documentType,
|
||
);
|
||
uploadedDocumentIds[documentType] = docId;
|
||
}
|
||
}
|
||
}
|
||
return { voiceId, uploadedDocumentIds };
|
||
}
|
||
|
||
private async createBlameDocument(
|
||
file: Express.Multer.File,
|
||
requestId: string,
|
||
documentType: BlameDocumentType,
|
||
): Promise<Types.ObjectId> {
|
||
const doc = await this.blameDocumentDbService.create({
|
||
path: file.path,
|
||
requestId: new Types.ObjectId(requestId),
|
||
fileName: file.filename,
|
||
documentType: documentType,
|
||
});
|
||
return (doc as any)._id;
|
||
}
|
||
|
||
private determinePartyAndValidate(
|
||
request: any,
|
||
userId: string,
|
||
): "firstParty" | "secondParty" {
|
||
const firstPartyId = request?.firstPartyDetails?.firstPartyId?.toString();
|
||
const secondPartyId =
|
||
request?.secondPartyDetails?.secondPartyId?.toString();
|
||
|
||
if (userId === firstPartyId) {
|
||
if (request.expertResendReply?.firstParty?.userReply) {
|
||
throw new BadRequestException(
|
||
"You have already submitted a reply for this resend request.",
|
||
);
|
||
}
|
||
return "firstParty";
|
||
}
|
||
|
||
if (userId === secondPartyId) {
|
||
if (request.expertResendReply?.secondParty?.userReply) {
|
||
throw new BadRequestException(
|
||
"You have already submitted a reply for this resend request.",
|
||
);
|
||
}
|
||
return "secondParty";
|
||
}
|
||
|
||
throw new ForbiddenException("User is not a party to this request.");
|
||
}
|
||
|
||
private async createVoiceDocument(
|
||
file: Express.Multer.File,
|
||
requestId: string,
|
||
context: UploadContext,
|
||
): Promise<Types.ObjectId> {
|
||
const voiceDoc = await this.blameVoiceDbService.create({
|
||
path: file.path,
|
||
requestId: new Types.ObjectId(requestId),
|
||
fileName: file.filename,
|
||
context: context,
|
||
});
|
||
return (voiceDoc as any)._id;
|
||
}
|
||
|
||
private async updateRequestStatusIfNeeded(requestId: string): Promise<void> {
|
||
const updatedRequest =
|
||
await this.requestManagementDbService.findOne(requestId);
|
||
const reply = updatedRequest.expertResendReply;
|
||
|
||
if (!reply) {
|
||
return; // No resend initiated, nothing to do.
|
||
}
|
||
|
||
// Helper function to check if a description contains a request for documents.
|
||
const doesDescriptionRequireDocuments = (
|
||
description: string | null | undefined,
|
||
): boolean => {
|
||
if (!description) return false;
|
||
|
||
const validDocTypes = Object.values(BlameDocumentType);
|
||
const descriptionParts = description.split(",").map((p) => p.trim());
|
||
|
||
// Return true if any part of the description matches a known document type.
|
||
return descriptionParts.some((part) =>
|
||
validDocTypes.some(
|
||
(docType) =>
|
||
docType.replace(/_/g, "").toUpperCase() === part.toUpperCase(),
|
||
),
|
||
);
|
||
};
|
||
|
||
// 1. Determine which parties were actually asked to resubmit DOCUMENTS.
|
||
const requiredPartiesToReply: ("firstParty" | "secondParty")[] = [];
|
||
|
||
if (
|
||
reply.firstParty &&
|
||
doesDescriptionRequireDocuments(reply.firstParty.firstPartyDescription)
|
||
) {
|
||
requiredPartiesToReply.push("firstParty");
|
||
}
|
||
if (
|
||
reply.secondParty &&
|
||
doesDescriptionRequireDocuments(reply.secondParty.secondPartyDescription)
|
||
) {
|
||
requiredPartiesToReply.push("secondParty");
|
||
}
|
||
|
||
if (requiredPartiesToReply.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const allRequiredRepliesAreIn = requiredPartiesToReply.every(
|
||
(party) => !!reply[party]?.userReply,
|
||
);
|
||
|
||
if (
|
||
allRequiredRepliesAreIn &&
|
||
updatedRequest.blameStatus !== ReqBlameStatus.CheckAgain
|
||
) {
|
||
await this.requestManagementDbService.findByIdAndUpdate(requestId, {
|
||
blameStatus: ReqBlameStatus.CheckAgain,
|
||
});
|
||
this.logger.log(
|
||
`Request ${requestId} status updated to CheckAgain as all required parties have resubmitted.`,
|
||
);
|
||
}
|
||
}
|
||
|
||
private async addedClientKey(data: { clientName: any; companyCode: number }) {
|
||
if (process.env.ADDED_AUTO_CLIENT_KEY) {
|
||
let clientExist = await this.clientService.findOne({
|
||
clientName: data.clientName,
|
||
});
|
||
if (!clientExist) {
|
||
try {
|
||
await this.clientService.addClient({
|
||
clientCode: data.companyCode,
|
||
clientName: {
|
||
persian: data.clientName,
|
||
english: null,
|
||
},
|
||
property: {
|
||
smsApiKey: null,
|
||
},
|
||
useExpertMode: "genuine",
|
||
});
|
||
} catch (err) {
|
||
console.log(err);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Verify that expert can access this file (only if they initiated it)
|
||
*/
|
||
private async verifyExpertAccess(
|
||
request: RequestManagementModel,
|
||
user: any,
|
||
): Promise<void> {
|
||
// If user is not an expert type, no verification needed (normal flow)
|
||
const expertRoles = [
|
||
RoleEnum.EXPERT,
|
||
RoleEnum.DAMAGE_EXPERT,
|
||
RoleEnum.FIELD_EXPERT,
|
||
];
|
||
if (!expertRoles.includes(user.role)) {
|
||
return;
|
||
}
|
||
|
||
// If file is not expert-initiated, experts cannot access it
|
||
if (!request.expertInitiated || !request.initiatedBy) {
|
||
throw new ForbiddenException(
|
||
"Experts can only access files they have initiated",
|
||
);
|
||
}
|
||
|
||
// Verify the expert is the one who initiated this file
|
||
if (String(request.initiatedBy) !== user.sub) {
|
||
throw new ForbiddenException(
|
||
"You can only access files that you have initiated",
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper method to add history event to a request
|
||
*/
|
||
private async addHistoryEvent(
|
||
requestId: string,
|
||
eventType: string,
|
||
actorId?: Types.ObjectId,
|
||
actorName?: string,
|
||
actorType?: string,
|
||
metadata?: any,
|
||
): Promise<void> {
|
||
const historyEvent: FileHistoryEvent = {
|
||
eventType,
|
||
actorId,
|
||
actorName,
|
||
actorType,
|
||
timestamp: new Date(),
|
||
metadata,
|
||
};
|
||
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
$push: { history: historyEvent },
|
||
},
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Expert creates an initial blame file (incomplete, waiting for completion)
|
||
*/
|
||
async createExpertInitiatedFile(
|
||
expert: any,
|
||
dto: CreateExpertInitiatedFileDto,
|
||
): Promise<{ requestId: string }> {
|
||
const reqNumber = new ShortUniqueId({ counter: 1 });
|
||
const expertId = new Types.ObjectId(expert.sub);
|
||
|
||
// For LINK method, validate phone numbers are provided
|
||
if (dto.creationMethod === CreationMethod.LINK) {
|
||
if (!dto.firstPartyPhoneNumber) {
|
||
throw new BadRequestException(
|
||
"First party phone number is required for LINK method",
|
||
);
|
||
}
|
||
if (dto.type === "THIRD_PARTY" && !dto.secondPartyPhoneNumber) {
|
||
throw new BadRequestException(
|
||
"Second party phone number is required for LINK method with THIRD_PARTY type",
|
||
);
|
||
}
|
||
if (
|
||
dto.type === "THIRD_PARTY" &&
|
||
dto.firstPartyPhoneNumber === dto.secondPartyPhoneNumber
|
||
) {
|
||
throw new BadRequestException(
|
||
"First and second party phone numbers must be different",
|
||
);
|
||
}
|
||
}
|
||
|
||
// For LINK method, create/get users and set their IDs
|
||
let firstPartyUserId: Types.ObjectId | undefined;
|
||
let secondPartyUserId: Types.ObjectId | undefined;
|
||
let firstPartyDetails: any = {};
|
||
let secondPartyDetails: any = {};
|
||
|
||
if (dto.creationMethod === CreationMethod.LINK) {
|
||
// Get or create first party user
|
||
firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||
dto.firstPartyPhoneNumber,
|
||
);
|
||
firstPartyDetails = {
|
||
firstPartyId: firstPartyUserId,
|
||
firstPartyPhoneNumber: dto.firstPartyPhoneNumber,
|
||
};
|
||
|
||
// Get or create second party user for THIRD_PARTY
|
||
if (dto.type === "THIRD_PARTY" && dto.secondPartyPhoneNumber) {
|
||
secondPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||
dto.secondPartyPhoneNumber,
|
||
);
|
||
secondPartyDetails = {
|
||
secondPartyId: secondPartyUserId,
|
||
secondPartyPhoneNumber: dto.secondPartyPhoneNumber,
|
||
};
|
||
}
|
||
}
|
||
|
||
const createRequest = await this.requestManagementDbService.create({
|
||
requestNumber: reqNumber.rnd(),
|
||
type: dto.type,
|
||
expertInitiated: true,
|
||
initiatedBy: expertId,
|
||
creationMethod: dto.creationMethod,
|
||
filledBy: dto.creationMethod === CreationMethod.IN_PERSON ? FilledBy.EXPERT : FilledBy.CUSTOMER,
|
||
currentStep: StepsEnum.createRequest,
|
||
nextStep:
|
||
dto.type === "THIRD_PARTY"
|
||
? StepsEnum.F_InitialForm
|
||
: StepsEnum.CarBodyForm,
|
||
blameStatus: ReqBlameStatus.PendingForFirstParty,
|
||
lockFile: false,
|
||
history: [],
|
||
assignedExpertId: expertId, // Auto-assign to initiating expert
|
||
...(dto.creationMethod === CreationMethod.LINK && {
|
||
firstPartyDetails,
|
||
...(dto.type === "THIRD_PARTY" && { secondPartyDetails }),
|
||
}),
|
||
});
|
||
|
||
const requestId = (createRequest as any)._id.toString();
|
||
|
||
// Add history event
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"FILE_CREATED_BY_EXPERT",
|
||
expertId,
|
||
`${expert.firstName} ${expert.lastName}`,
|
||
"expert",
|
||
{
|
||
creationMethod: dto.creationMethod,
|
||
type: dto.type,
|
||
},
|
||
);
|
||
|
||
return {
|
||
requestId,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* List all expert-initiated blame files for the current field expert (their panel).
|
||
* Only files where initiatedBy === current user are returned.
|
||
*/
|
||
async getMyExpertInitiatedFiles(expert: any): Promise<any[]> {
|
||
const expertId = new Types.ObjectId(expert.sub);
|
||
const files = await this.requestManagementDbService.findAll({
|
||
expertInitiated: true,
|
||
initiatedBy: expertId,
|
||
});
|
||
return (files || []).map((f) => ({
|
||
_id: f._id,
|
||
requestNumber: f.requestNumber,
|
||
type: f.type,
|
||
creationMethod: f.creationMethod,
|
||
blameStatus: f.blameStatus,
|
||
currentStep: f.currentStep,
|
||
nextStep: f.nextStep,
|
||
createdAt: f.createdAt,
|
||
steps: f.steps,
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Single endpoint handler: complete all blame-needed data.
|
||
* Routes to THIRD_PARTY or CAR_BODY completion based on request type.
|
||
*/
|
||
async completeBlameData(
|
||
expert: any,
|
||
requestId: string,
|
||
formData: any,
|
||
): Promise<RequestManagementDtoRs> {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
if (!request.expertInitiated) {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for expert-initiated files",
|
||
);
|
||
}
|
||
await this.verifyExpertAccess(request, expert);
|
||
|
||
if (request.type === "THIRD_PARTY") {
|
||
return this.expertCompleteThirdPartyForm(expert, requestId, formData);
|
||
}
|
||
if (request.type === "CAR_BODY") {
|
||
return this.expertCompleteCarBodyForm(expert, requestId, formData);
|
||
}
|
||
throw new BadRequestException(
|
||
"Unknown file type. Must be THIRD_PARTY or CAR_BODY.",
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Expert completes all information for IN_PERSON THIRD_PARTY file at once
|
||
* This replaces the step-by-step flow for experts filling files on-site
|
||
*/
|
||
async expertCompleteThirdPartyForm(
|
||
expert: any,
|
||
requestId: string,
|
||
formData: any, // ExpertCompleteThirdPartyFormDto
|
||
): Promise<RequestManagementDtoRs> {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// Verify this is an expert-initiated IN_PERSON file
|
||
if (!request.expertInitiated) {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for expert-initiated files",
|
||
);
|
||
}
|
||
|
||
if (request.creationMethod !== CreationMethod.IN_PERSON) {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for IN_PERSON files. LINK-based files should be completed by users step-by-step.",
|
||
);
|
||
}
|
||
|
||
// Verify expert access
|
||
await this.verifyExpertAccess(request, expert);
|
||
|
||
// Validate second party is provided for THIRD_PARTY type
|
||
if (request.type === "THIRD_PARTY" && !formData.secondParty) {
|
||
throw new BadRequestException(
|
||
"Second party information is required for THIRD_PARTY type files",
|
||
);
|
||
}
|
||
|
||
// Validate guilty party is provided for THIRD_PARTY type
|
||
if (request.type === "THIRD_PARTY" && !formData.guiltyPartyPhoneNumber) {
|
||
throw new BadRequestException(
|
||
"Guilty party phone number is required for THIRD_PARTY type files",
|
||
);
|
||
}
|
||
|
||
// Validate carBodyForm is provided for CAR_BODY type
|
||
if (request.type === "CAR_BODY" && !formData.carBodyForm) {
|
||
throw new BadRequestException(
|
||
"CAR_BODY form is required for CAR_BODY type files",
|
||
);
|
||
}
|
||
|
||
// Validate phone numbers are different
|
||
if (
|
||
request.type === "THIRD_PARTY" &&
|
||
formData.firstPartyPhoneNumber === formData.secondParty.phoneNumber
|
||
) {
|
||
throw new BadRequestException(
|
||
"First and second party phone numbers must be different",
|
||
);
|
||
}
|
||
|
||
// Validate guilty party phone number matches one of the parties
|
||
if (request.type === "THIRD_PARTY") {
|
||
const guiltyMatchesFirst =
|
||
formData.guiltyPartyPhoneNumber === formData.firstPartyPhoneNumber;
|
||
const guiltyMatchesSecond =
|
||
formData.guiltyPartyPhoneNumber === formData.secondParty.phoneNumber;
|
||
if (!guiltyMatchesFirst && !guiltyMatchesSecond) {
|
||
throw new BadRequestException(
|
||
"Guilty party phone number must match either first or second party phone number",
|
||
);
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Get or create user for first party phone number
|
||
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||
formData.firstPartyPhoneNumber,
|
||
);
|
||
|
||
// Collect all steps to be added
|
||
const stepsToAdd: StepsEnum[] = [StepsEnum.F_InitialForm];
|
||
|
||
// Initialize firstPartyDetails structure completely
|
||
const firstPartyDetails: any = {
|
||
firstPartyId: firstPartyUserId,
|
||
firstPartyPhoneNumber: formData.firstPartyPhoneNumber,
|
||
firstPartyInitialForm: {
|
||
expertOpinion: formData.firstPartyInitialForm.expertOpinion,
|
||
imDamaged: formData.firstPartyInitialForm.imDamaged,
|
||
imGuilty: formData.firstPartyInitialForm.imGuilty,
|
||
},
|
||
firstPartyFile: {
|
||
descriptions: [formData.firstPartyDescription.desc],
|
||
},
|
||
};
|
||
|
||
// Get or create user for second party phone number
|
||
const secondPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||
formData.secondParty.phoneNumber,
|
||
);
|
||
|
||
const secondPartyDetails: any = {
|
||
secondPartyId: secondPartyUserId,
|
||
secondPartyPhoneNumber: formData.secondParty.phoneNumber,
|
||
secondPartyInitialForm: {
|
||
expertOpinion: formData.secondParty.initialForm.expertOpinion,
|
||
imDamaged: formData.secondParty.initialForm.imDamaged,
|
||
imGuilty: formData.secondParty.initialForm.imGuilty,
|
||
},
|
||
secondPartyFiles: {
|
||
descriptions: [formData.secondParty.description.desc],
|
||
},
|
||
};
|
||
|
||
// Build comprehensive update payload
|
||
const updatePayload: any = {
|
||
$set: {
|
||
filledBy: FilledBy.EXPERT,
|
||
firstPartyDetails: firstPartyDetails,
|
||
firstPartyLocation: formData.firstPartyLocation,
|
||
secondPartyDetails: secondPartyDetails,
|
||
secondPartyLocation: formData.secondParty.location,
|
||
},
|
||
};
|
||
|
||
// Add steps for location and description
|
||
stepsToAdd.push(StepsEnum.F_addLocation);
|
||
stepsToAdd.push(StepsEnum.F_addDescription);
|
||
stepsToAdd.push(StepsEnum.S_InitialForm);
|
||
stepsToAdd.push(StepsEnum.S_addLocation);
|
||
stepsToAdd.push(StepsEnum.S_addDescription);
|
||
|
||
// Process first party plate with SandHub lookup
|
||
const firstPartyPlate = formData.firstPartyPlate;
|
||
try {
|
||
const sandHubResponse = await this.sandHubService.getSandHubResponse({
|
||
plate: firstPartyPlate.plate,
|
||
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
||
});
|
||
const sandHubReport = sandHubResponse["_doc"] || sandHubResponse;
|
||
|
||
const clientName = sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
|
||
const companyCode = sandHubReport?.CompanyCode;
|
||
|
||
// Try to find client by company code first (more reliable)
|
||
const client = companyCode
|
||
? await this.clientService.findClientWithCompanyCode(+companyCode)
|
||
: await this.clientService.findOne({ clientName: clientName });
|
||
|
||
if (!client) {
|
||
throw new NotFoundException(
|
||
`Client not found for company: ${clientName}`,
|
||
);
|
||
}
|
||
|
||
// Add first party plate and related data to the existing firstPartyDetails object
|
||
firstPartyDetails.nationalCodeOfInsurer = firstPartyPlate.nationalCodeOfInsurer;
|
||
firstPartyDetails.firstPartyPlate = firstPartyPlate.plate;
|
||
firstPartyDetails.firstPartyCarDetail = {
|
||
carName: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||
insurerBirthday: firstPartyPlate.insurerBirthday,
|
||
driverBirthday: firstPartyPlate.driverBirthday,
|
||
carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||
isNewCar: firstPartyPlate.isNewCar,
|
||
};
|
||
firstPartyDetails.firstPartyClient = {
|
||
clientId: (client as any)["_doc"]?._id || (client as any)._id,
|
||
clientName: clientName,
|
||
};
|
||
firstPartyDetails.firstPartyCertificate = firstPartyPlate.userNoCertificate;
|
||
firstPartyDetails.firstPartyInsuranceDetail = {
|
||
insuranceId: sandHubReport?.LastCompanyDocumentNumber || sandHubReport?.DocumentNumber,
|
||
insuranceCompany: clientName,
|
||
financialCommitmentCeiling: sandHubReport?.FinancialCvrCptl || sandHubReport?.FinancialCommitment,
|
||
startDate: sandHubReport?.IssueDate || sandHubReport?.StartDate,
|
||
endDate: sandHubReport?.EndDate,
|
||
};
|
||
|
||
// Update the firstPartyDetails in the payload with all plate data
|
||
updatePayload.$set.firstPartyDetails = firstPartyDetails;
|
||
|
||
stepsToAdd.push(StepsEnum.F_addPlate);
|
||
} catch (plateError) {
|
||
this.logger.error("Error processing first party plate:", plateError);
|
||
throw new InternalServerErrorException(
|
||
"Failed to process first party plate information",
|
||
);
|
||
}
|
||
|
||
// Process second party plate
|
||
try {
|
||
const secondPartyPlate = formData.secondParty.plate;
|
||
const sandHubResponse = await this.sandHubService.getSandHubResponse({
|
||
plate: secondPartyPlate.plate,
|
||
nationalCodeOfInsurer: secondPartyPlate.nationalCodeOfInsurer,
|
||
});
|
||
const sandHubReport = sandHubResponse["_doc"] || sandHubResponse;
|
||
|
||
const clientName = sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
|
||
const companyCode = sandHubReport?.CompanyCode;
|
||
|
||
// Try to find client by company code first (more reliable)
|
||
const client = companyCode
|
||
? await this.clientService.findClientWithCompanyCode(+companyCode)
|
||
: await this.clientService.findOne({ clientName: clientName });
|
||
|
||
if (!client) {
|
||
throw new NotFoundException(
|
||
`Client not found for company: ${clientName}${companyCode ? ` (code: ${companyCode})` : ""}`,
|
||
);
|
||
}
|
||
|
||
// Add second party plate and related data to the existing secondPartyDetails object
|
||
secondPartyDetails.nationalCodeOfInsurer = secondPartyPlate.nationalCodeOfInsurer;
|
||
secondPartyDetails.secondPartyPlate = secondPartyPlate.plate;
|
||
secondPartyDetails.secondPartyCarDetail = {
|
||
carName: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||
insurerBirthday: secondPartyPlate.insurerBirthday,
|
||
driverBirthday: secondPartyPlate.driverBirthday,
|
||
carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||
isNewCar: secondPartyPlate.isNewCar,
|
||
};
|
||
secondPartyDetails.secondPartyClient = {
|
||
clientId: (client as any)["_doc"]?._id || (client as any)._id,
|
||
clientName: clientName,
|
||
};
|
||
secondPartyDetails.secondPartyCertificate = secondPartyPlate.userNoCertificate;
|
||
secondPartyDetails.secondPartyInsuranceDetail = {
|
||
insuranceId: sandHubReport?.LastCompanyDocumentNumber || sandHubReport?.DocumentNumber,
|
||
insuranceCompany: clientName,
|
||
financialCommitmentCeiling: sandHubReport?.FinancialCvrCptl || sandHubReport?.FinancialCommitment,
|
||
startDate: sandHubReport?.IssueDate || sandHubReport?.StartDate,
|
||
endDate: sandHubReport?.EndDate,
|
||
};
|
||
|
||
// Update the secondPartyDetails in the payload
|
||
updatePayload.$set.secondPartyDetails = secondPartyDetails;
|
||
|
||
stepsToAdd.push(StepsEnum.S_addPlate);
|
||
} catch (plateError) {
|
||
this.logger.error("Error processing second party plate:", plateError);
|
||
throw new InternalServerErrorException(
|
||
"Failed to process second party plate information",
|
||
);
|
||
}
|
||
|
||
// Determine which party is guilty based on phone number
|
||
const guiltyMatchesFirst =
|
||
formData.guiltyPartyPhoneNumber === formData.firstPartyPhoneNumber;
|
||
const guiltyUserId = guiltyMatchesFirst
|
||
? firstPartyUserId
|
||
: secondPartyDetails.secondPartyId;
|
||
|
||
// Create initial expertSubmitReply with guiltyUserId
|
||
// Accident fields can be added separately via add-accident-fields endpoint
|
||
// This allows the file to proceed directly to signatures without expert review
|
||
updatePayload.$set.expertSubmitReply = {
|
||
description: "Expert completed all information on-site. Awaiting signatures.",
|
||
guiltyUserId: guiltyUserId,
|
||
submitTime: new Date(),
|
||
fields: {
|
||
accidentWay: {
|
||
id: "",
|
||
label: "To be determined",
|
||
},
|
||
accidentReason: {
|
||
id: "",
|
||
label: "To be determined",
|
||
fanavaran: 0,
|
||
},
|
||
accidentType: {
|
||
id: "",
|
||
label: "To be determined",
|
||
},
|
||
},
|
||
firstPartyComment: null,
|
||
secondPartyComment: null,
|
||
};
|
||
|
||
updatePayload.$set.currentStep = StepsEnum.S_addDescription;
|
||
updatePayload.$set.nextStep = StepsEnum.AddSignatures;
|
||
updatePayload.$set.blameStatus = ReqBlameStatus.WaitingForSignatures;
|
||
|
||
// Add all steps at once using $each to push multiple values
|
||
if (stepsToAdd.length > 0) {
|
||
updatePayload.$push = {
|
||
steps: { $each: stepsToAdd },
|
||
};
|
||
}
|
||
|
||
// Execute the update
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
updatePayload,
|
||
);
|
||
|
||
// Ensure expert assignment
|
||
await this.ensureExpertAssignment(requestId);
|
||
|
||
// Add history events
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"EXPERT_COMPLETED_ALL_INFORMATION",
|
||
new Types.ObjectId(expert.sub),
|
||
undefined,
|
||
"expert",
|
||
{
|
||
type: "THIRD_PARTY",
|
||
hasSecondParty: true,
|
||
},
|
||
);
|
||
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"READY_FOR_SIGNATURES",
|
||
new Types.ObjectId(expert.sub),
|
||
undefined,
|
||
"system",
|
||
);
|
||
|
||
// Read updated request
|
||
const updatedRequest = await this.requestManagementDbService.findOne(
|
||
requestId,
|
||
);
|
||
|
||
return new RequestManagementDtoRs(
|
||
updatedRequest.firstPartyDetails?.firstPartyId,
|
||
updatedRequest?.secondPartyDetails?.secondPartyId || null,
|
||
StepsEnum.S_addDescription,
|
||
StepsEnum.AddSignatures,
|
||
"All information completed. Waiting for user signatures.",
|
||
updatedRequest._id.toString(),
|
||
);
|
||
} catch (error) {
|
||
this.logger.error("Error in expertCompleteForm:", error);
|
||
if (error instanceof HttpException) {
|
||
throw error;
|
||
}
|
||
throw new InternalServerErrorException(
|
||
"Failed to complete form. Please try again.",
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Expert completes all information for IN_PERSON CAR_BODY file at once
|
||
* This replaces the step-by-step flow for experts filling files on-site
|
||
*/
|
||
async expertCompleteCarBodyForm(
|
||
expert: any,
|
||
requestId: string,
|
||
formData: any, // ExpertCompleteCarBodyFormDto
|
||
): Promise<RequestManagementDtoRs> {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// Verify this is an expert-initiated IN_PERSON CAR_BODY file
|
||
if (!request.expertInitiated) {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for expert-initiated files",
|
||
);
|
||
}
|
||
|
||
if (request.creationMethod !== CreationMethod.IN_PERSON) {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for IN_PERSON files. LINK-based files should be completed by users step-by-step.",
|
||
);
|
||
}
|
||
|
||
if (request.type !== "CAR_BODY") {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for CAR_BODY type files. Use complete-form-third-party for THIRD_PARTY files.",
|
||
);
|
||
}
|
||
|
||
// Verify expert access
|
||
await this.verifyExpertAccess(request, expert);
|
||
|
||
// Validate carBodyForm is provided
|
||
if (!formData.carBodyForm) {
|
||
throw new BadRequestException(
|
||
"CAR_BODY form is required for CAR_BODY type files",
|
||
);
|
||
}
|
||
|
||
try {
|
||
// Get or create user for first party phone number
|
||
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||
formData.firstPartyPhoneNumber,
|
||
);
|
||
|
||
// Collect all steps to be added
|
||
const stepsToAdd: StepsEnum[] = [StepsEnum.CarBodyForm];
|
||
|
||
// Initialize firstPartyDetails structure completely
|
||
const firstPartyDetails: any = {
|
||
firstPartyId: firstPartyUserId,
|
||
firstPartyPhoneNumber: formData.firstPartyPhoneNumber,
|
||
firstPartyInitialForm: {
|
||
expertOpinion: formData.firstPartyInitialForm.expertOpinion,
|
||
imDamaged: formData.firstPartyInitialForm.imDamaged,
|
||
imGuilty: formData.firstPartyInitialForm.imGuilty,
|
||
},
|
||
firstPartyFile: {
|
||
descriptions: [formData.firstPartyDescription.desc],
|
||
},
|
||
carBodyForm: {
|
||
firstPartyId: firstPartyUserId,
|
||
firstPartyPhoneNumber: formData.firstPartyPhoneNumber,
|
||
carBodyForm: formData.carBodyForm,
|
||
// Assume guilty by default if accident was with another car
|
||
isGuilty: formData.carBodyForm.car ? true : false,
|
||
},
|
||
};
|
||
|
||
// CAR_BODY specific fields
|
||
if (formData.firstPartyDescription.accidentDate) {
|
||
firstPartyDetails.firstPartyFile.accidentDate = formData.firstPartyDescription.accidentDate;
|
||
}
|
||
if (formData.firstPartyDescription.accidentTime) {
|
||
firstPartyDetails.firstPartyFile.accidentTime = formData.firstPartyDescription.accidentTime;
|
||
}
|
||
if (formData.firstPartyDescription.weatherCondition) {
|
||
firstPartyDetails.firstPartyFile.weatherCondition = formData.firstPartyDescription.weatherCondition;
|
||
}
|
||
if (formData.firstPartyDescription.roadCondition) {
|
||
firstPartyDetails.firstPartyFile.roadCondition = formData.firstPartyDescription.roadCondition;
|
||
}
|
||
if (formData.firstPartyDescription.lightCondition) {
|
||
firstPartyDetails.firstPartyFile.lightCondition = formData.firstPartyDescription.lightCondition;
|
||
}
|
||
|
||
// Build comprehensive update payload
|
||
const updatePayload: any = {
|
||
$set: {
|
||
filledBy: FilledBy.EXPERT,
|
||
firstPartyDetails: firstPartyDetails,
|
||
firstPartyLocation: formData.firstPartyLocation,
|
||
},
|
||
};
|
||
|
||
// Add steps for location and description
|
||
stepsToAdd.push(StepsEnum.F_addLocation);
|
||
stepsToAdd.push(StepsEnum.F_addDescription);
|
||
|
||
// Process first party plate with SandHub lookup
|
||
const firstPartyPlate = formData.firstPartyPlate;
|
||
try {
|
||
const sandHubResponse = await this.sandHubService.getSandHubResponse({
|
||
plate: firstPartyPlate.plate,
|
||
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
||
});
|
||
const sandHubReport = sandHubResponse["_doc"] || sandHubResponse;
|
||
|
||
const clientName = sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
|
||
const companyCode = sandHubReport?.CompanyCode;
|
||
|
||
// Try to find client by company code first (more reliable)
|
||
const client = companyCode
|
||
? await this.clientService.findClientWithCompanyCode(+companyCode)
|
||
: await this.clientService.findOne({ clientName: clientName });
|
||
|
||
if (!client) {
|
||
throw new NotFoundException(
|
||
`Client not found for company: ${clientName}`,
|
||
);
|
||
}
|
||
|
||
// Add first party plate and related data to the existing firstPartyDetails object
|
||
firstPartyDetails.nationalCodeOfInsurer = firstPartyPlate.nationalCodeOfInsurer;
|
||
firstPartyDetails.firstPartyPlate = firstPartyPlate.plate;
|
||
firstPartyDetails.firstPartyCarDetail = {
|
||
carName: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||
insurerBirthday: firstPartyPlate.insurerBirthday,
|
||
driverBirthday: firstPartyPlate.driverBirthday,
|
||
carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||
isNewCar: firstPartyPlate.isNewCar,
|
||
};
|
||
firstPartyDetails.firstPartyClient = {
|
||
clientId: (client as any)["_doc"]?._id || (client as any)._id,
|
||
clientName: clientName,
|
||
};
|
||
firstPartyDetails.firstPartyCertificate = firstPartyPlate.userNoCertificate;
|
||
firstPartyDetails.firstPartyInsuranceDetail = {
|
||
insuranceId: sandHubReport?.LastCompanyDocumentNumber || sandHubReport?.DocumentNumber,
|
||
insuranceCompany: clientName,
|
||
financialCommitmentCeiling: sandHubReport?.FinancialCvrCptl || sandHubReport?.FinancialCommitment,
|
||
startDate: sandHubReport?.IssueDate || sandHubReport?.StartDate,
|
||
endDate: sandHubReport?.EndDate,
|
||
};
|
||
|
||
// CAR_BODY specific insurance info
|
||
const carBodyInfo = await this.mockCarBodyInsuranceInquiry(
|
||
request._id.toString(),
|
||
firstPartyPlate.plate,
|
||
firstPartyPlate.nationalCodeOfInsurer,
|
||
);
|
||
|
||
firstPartyDetails.firstPartyCarBodyInsuranceDetail = {
|
||
policyNumber: carBodyInfo.policyNumber,
|
||
startDate: carBodyInfo.startDate,
|
||
endDate: carBodyInfo.endDate,
|
||
insurerCompany: carBodyInfo.insurerCompany,
|
||
coverages: carBodyInfo.coverages,
|
||
};
|
||
|
||
// Update the firstPartyDetails in the payload with all plate data
|
||
updatePayload.$set.firstPartyDetails = firstPartyDetails;
|
||
|
||
stepsToAdd.push(StepsEnum.F_addPlate);
|
||
} catch (plateError) {
|
||
this.logger.error("Error processing first party plate:", plateError);
|
||
throw new InternalServerErrorException(
|
||
"Failed to process first party plate information",
|
||
);
|
||
}
|
||
|
||
// For CAR_BODY: Create expertSubmitReply
|
||
// First party is guilty by default if accident was with another car
|
||
const guiltyUserId = firstPartyUserId;
|
||
|
||
// Accident fields can be added separately via add-accident-fields endpoint
|
||
updatePayload.$set.expertSubmitReply = {
|
||
description: "Expert completed all information on-site. Awaiting signature.",
|
||
guiltyUserId: guiltyUserId,
|
||
submitTime: new Date(),
|
||
fields: {
|
||
accidentWay: {
|
||
id: "",
|
||
label: "To be determined",
|
||
},
|
||
accidentReason: {
|
||
id: "",
|
||
label: "To be determined",
|
||
fanavaran: 0,
|
||
},
|
||
accidentType: {
|
||
id: "",
|
||
label: "To be determined",
|
||
},
|
||
},
|
||
firstPartyComment: null,
|
||
secondPartyComment: null,
|
||
};
|
||
|
||
updatePayload.$set.currentStep = StepsEnum.F_addDescription;
|
||
updatePayload.$set.nextStep = StepsEnum.AddSignatures;
|
||
updatePayload.$set.blameStatus = ReqBlameStatus.WaitingForSignatures;
|
||
|
||
// Add all steps at once using $each to push multiple values
|
||
if (stepsToAdd.length > 0) {
|
||
updatePayload.$push = {
|
||
steps: { $each: stepsToAdd },
|
||
};
|
||
}
|
||
|
||
// Execute the update
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
updatePayload,
|
||
);
|
||
|
||
// Ensure expert assignment
|
||
await this.ensureExpertAssignment(requestId);
|
||
|
||
// Add history events
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"EXPERT_COMPLETED_ALL_INFORMATION",
|
||
new Types.ObjectId(expert.sub),
|
||
undefined,
|
||
"expert",
|
||
{
|
||
type: "CAR_BODY",
|
||
hasSecondParty: false,
|
||
},
|
||
);
|
||
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"READY_FOR_SIGNATURES",
|
||
new Types.ObjectId(expert.sub),
|
||
undefined,
|
||
"system",
|
||
);
|
||
|
||
// Read updated request
|
||
const updatedRequest = await this.requestManagementDbService.findOne(
|
||
requestId,
|
||
);
|
||
|
||
return new RequestManagementDtoRs(
|
||
updatedRequest.firstPartyDetails?.firstPartyId,
|
||
null,
|
||
StepsEnum.F_addDescription,
|
||
StepsEnum.AddSignatures,
|
||
"All information completed. Waiting for user signature.",
|
||
updatedRequest._id.toString(),
|
||
);
|
||
} catch (error) {
|
||
this.logger.error("Error in expertCompleteCarBodyForm:", error);
|
||
if (error instanceof HttpException) {
|
||
throw error;
|
||
}
|
||
throw new InternalServerErrorException(
|
||
"Failed to complete form. Please try again.",
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Expert uploads video for expert-initiated file
|
||
* Only one video is needed per file (not per party)
|
||
*/
|
||
async expertUploadVideo(
|
||
expert: any,
|
||
requestId: string,
|
||
file: Express.Multer.File,
|
||
): Promise<RequestManagementDtoRs> {
|
||
if (!file) {
|
||
throw new BadRequestException("Video file is required");
|
||
}
|
||
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// Verify this is an expert-initiated file
|
||
if (!request.expertInitiated) {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for expert-initiated files",
|
||
);
|
||
}
|
||
|
||
// Verify expert access
|
||
await this.verifyExpertAccess(request, expert);
|
||
|
||
// Check if video already exists
|
||
const stepToCheck = request.type === "THIRD_PARTY" ? StepsEnum.F_videoUpload : StepsEnum.CarBodyVideo;
|
||
if (request.steps?.includes(stepToCheck)) {
|
||
throw new BadRequestException("Video has already been uploaded for this file");
|
||
}
|
||
|
||
try {
|
||
// Create video document
|
||
const videoDocument = await this.blameVideoDbService.create({
|
||
fileName: file.filename,
|
||
path: file.path,
|
||
requestId: new Types.ObjectId(requestId),
|
||
});
|
||
|
||
// Determine the step based on request type
|
||
const stepToPush = request.type === "THIRD_PARTY" ? StepsEnum.F_videoUpload : StepsEnum.CarBodyVideo;
|
||
const currentStepValue = stepToPush;
|
||
const nextStepValue = StepsEnum.F_addPlate;
|
||
|
||
// Update request with video
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
$push: { steps: stepToPush },
|
||
$set: {
|
||
currentStep: currentStepValue,
|
||
"firstPartyDetails.firstPartyFile.firstPartyVideoId": videoDocument["_doc"]._id,
|
||
nextStep: nextStepValue,
|
||
},
|
||
},
|
||
);
|
||
|
||
// Add history event
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"EXPERT_UPLOADED_VIDEO",
|
||
new Types.ObjectId(expert.sub),
|
||
undefined,
|
||
"expert",
|
||
{
|
||
videoId: videoDocument["_doc"]._id.toString(),
|
||
fileName: file.filename,
|
||
},
|
||
);
|
||
|
||
return new RequestManagementDtoRs(
|
||
request.firstPartyDetails?.firstPartyId || new Types.ObjectId(expert.sub),
|
||
request?.secondPartyDetails?.secondPartyId || null,
|
||
currentStepValue,
|
||
nextStepValue,
|
||
"Video uploaded successfully",
|
||
request._id,
|
||
);
|
||
} catch (error) {
|
||
this.logger.error("Error uploading expert video:", error);
|
||
if (error instanceof HttpException) {
|
||
throw error;
|
||
}
|
||
throw new InternalServerErrorException("Failed to upload video");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Expert uploads voice for expert-initiated file
|
||
* Only one voice is needed per file (not per party)
|
||
*/
|
||
async expertUploadVoice(
|
||
expert: any,
|
||
requestId: string,
|
||
voice: Express.Multer.File,
|
||
): Promise<RequestManagementDtoRs> {
|
||
if (!voice) {
|
||
throw new BadRequestException("Voice file is required");
|
||
}
|
||
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// Verify this is an expert-initiated file
|
||
if (!request.expertInitiated) {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for expert-initiated files",
|
||
);
|
||
}
|
||
|
||
// Verify expert access
|
||
await this.verifyExpertAccess(request, expert);
|
||
|
||
// Check if voice already exists (check both first and second party steps)
|
||
if (
|
||
request.steps?.includes(StepsEnum.F_addVoice) ||
|
||
request.steps?.includes(StepsEnum.S_addVoice)
|
||
) {
|
||
throw new BadRequestException("Voice has already been uploaded for this file");
|
||
}
|
||
|
||
try {
|
||
// Create voice document
|
||
const voiceDoc = await this.blameVoiceDbService.create({
|
||
fileName: voice.filename,
|
||
path: voice.path,
|
||
requestId: new Types.ObjectId(requestId),
|
||
context: UploadContext.INITIAL, // Use INITIAL context for expert uploads
|
||
});
|
||
|
||
// Determine current step and next step based on request state
|
||
// If description is already added, next step is different
|
||
const hasDescription = request.steps?.includes(StepsEnum.F_addDescription) ||
|
||
request.steps?.includes(StepsEnum.S_addDescription);
|
||
|
||
const currentStep = request.type === "THIRD_PARTY"
|
||
? StepsEnum.F_addVoice
|
||
: StepsEnum.F_addVoice;
|
||
|
||
const nextStep = hasDescription
|
||
? (request.type === "THIRD_PARTY" ? StepsEnum.AddSignatures : StepsEnum.AddSignatures)
|
||
: (request.type === "THIRD_PARTY" ? StepsEnum.F_addDescription : StepsEnum.F_addDescription);
|
||
|
||
// Update request with voice
|
||
// For expert uploads, we store it in firstPartyFile but it represents the entire file
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
$push: {
|
||
"firstPartyDetails.firstPartyFile.voices": voiceDoc["_id"],
|
||
steps: StepsEnum.F_addVoice,
|
||
},
|
||
$set: {
|
||
currentStep: currentStep,
|
||
nextStep: nextStep,
|
||
},
|
||
},
|
||
);
|
||
|
||
// Add history event
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"EXPERT_UPLOADED_VOICE",
|
||
new Types.ObjectId(expert.sub),
|
||
undefined,
|
||
"expert",
|
||
{
|
||
voiceId: voiceDoc["_id"].toString(),
|
||
fileName: voice.filename,
|
||
},
|
||
);
|
||
|
||
const message = hasDescription
|
||
? "Voice uploaded successfully. File is ready for signatures."
|
||
: "Voice uploaded successfully. Please add description.";
|
||
|
||
return new RequestManagementDtoRs(
|
||
request.firstPartyDetails?.firstPartyId || new Types.ObjectId(expert.sub),
|
||
request?.secondPartyDetails?.secondPartyId || null,
|
||
currentStep,
|
||
nextStep,
|
||
message,
|
||
request._id,
|
||
);
|
||
} catch (error) {
|
||
this.logger.error("Error uploading expert voice:", error);
|
||
if (error instanceof HttpException) {
|
||
throw error;
|
||
}
|
||
throw new InternalServerErrorException("Failed to upload voice");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Expert adds accident fields to expert-initiated file
|
||
* Can be used for both IN_PERSON and LINK-based files
|
||
*/
|
||
async expertAddAccidentFields(
|
||
expert: any,
|
||
requestId: string,
|
||
fields: { accidentWay: any; accidentReason: any; accidentType: any },
|
||
): Promise<{ message: string; requestId: string }> {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
if (!request) {
|
||
throw new NotFoundException("Request not found");
|
||
}
|
||
|
||
// Verify this is an expert-initiated file
|
||
if (!request.expertInitiated) {
|
||
throw new BadRequestException(
|
||
"This endpoint is only for expert-initiated files",
|
||
);
|
||
}
|
||
|
||
// Verify expert access
|
||
await this.verifyExpertAccess(request, expert);
|
||
|
||
// Determine which reply field to update
|
||
const isObjection = !!request.expertResendReply;
|
||
const replyField = isObjection
|
||
? "expertSubmitReplyFinal"
|
||
: "expertSubmitReply";
|
||
|
||
// Get existing reply or create new structure
|
||
const existingReply = request[replyField] || {};
|
||
const newReplyObject: any = {
|
||
...existingReply,
|
||
fields: {
|
||
accidentWay: fields.accidentWay,
|
||
accidentReason: fields.accidentReason,
|
||
accidentType: fields.accidentType,
|
||
},
|
||
};
|
||
|
||
// Preserve other fields if they exist (for SubmitReply structure)
|
||
if (existingReply && typeof existingReply === 'object') {
|
||
if ('description' in existingReply) {
|
||
newReplyObject.description = existingReply.description;
|
||
} else {
|
||
newReplyObject.description = "Expert on-site assessment";
|
||
}
|
||
|
||
if ('guiltyUserId' in existingReply) {
|
||
newReplyObject.guiltyUserId = existingReply.guiltyUserId;
|
||
} else {
|
||
newReplyObject.guiltyUserId = request.firstPartyDetails?.firstPartyId;
|
||
}
|
||
|
||
if ('submitTime' in existingReply) {
|
||
newReplyObject.submitTime = existingReply.submitTime;
|
||
} else {
|
||
newReplyObject.submitTime = new Date();
|
||
}
|
||
|
||
if ('firstPartyComment' in existingReply) {
|
||
newReplyObject.firstPartyComment = existingReply.firstPartyComment;
|
||
} else {
|
||
newReplyObject.firstPartyComment = null;
|
||
}
|
||
|
||
if ('secondPartyComment' in existingReply) {
|
||
newReplyObject.secondPartyComment = existingReply.secondPartyComment;
|
||
} else {
|
||
newReplyObject.secondPartyComment = null;
|
||
}
|
||
} else {
|
||
// Create new structure if no existing reply
|
||
newReplyObject.description = "Expert on-site assessment";
|
||
newReplyObject.guiltyUserId = request.firstPartyDetails?.firstPartyId;
|
||
newReplyObject.submitTime = new Date();
|
||
newReplyObject.firstPartyComment = null;
|
||
newReplyObject.secondPartyComment = null;
|
||
}
|
||
|
||
// Update the file
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
$set: {
|
||
[replyField]: newReplyObject,
|
||
},
|
||
},
|
||
);
|
||
|
||
// Add history event
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"EXPERT_ADDED_ACCIDENT_FIELDS",
|
||
new Types.ObjectId(expert.sub),
|
||
undefined,
|
||
"expert",
|
||
{
|
||
hasObjection: isObjection,
|
||
},
|
||
);
|
||
|
||
return {
|
||
message: "Accident fields added successfully",
|
||
requestId: requestId,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Check if request is expert-initiated and should be auto-assigned
|
||
*/
|
||
async ensureExpertAssignment(requestId: string): Promise<void> {
|
||
const request = await this.requestManagementDbService.findOne(requestId);
|
||
|
||
if (request?.expertInitiated && request.initiatedBy) {
|
||
// Ensure the file is assigned to the initiating expert
|
||
if (
|
||
!request.assignedExpertId ||
|
||
String(request.assignedExpertId) !== String(request.initiatedBy)
|
||
) {
|
||
await this.requestManagementDbService.findAndUpdate(
|
||
{ _id: requestId },
|
||
{
|
||
$set: {
|
||
assignedExpertId: request.initiatedBy,
|
||
},
|
||
},
|
||
);
|
||
|
||
// Add history event
|
||
await this.addHistoryEvent(
|
||
requestId,
|
||
"AUTO_ASSIGNED_TO_EXPERT",
|
||
request.initiatedBy,
|
||
undefined,
|
||
"system",
|
||
{
|
||
expertId: request.initiatedBy.toString(),
|
||
},
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get or create a user by phone number
|
||
* Used for IN_PERSON expert-initiated files where expert provides phone numbers
|
||
*/
|
||
private async getOrCreateUserByPhoneNumber(phoneNumber: string): Promise<Types.ObjectId> {
|
||
// Try to find existing user by phone number (username or mobile)
|
||
let user = await this.userDbService.findOne({
|
||
$or: [
|
||
{ username: phoneNumber },
|
||
{ mobile: phoneNumber },
|
||
],
|
||
});
|
||
|
||
if (user) {
|
||
return new Types.ObjectId((user as any)._id);
|
||
}
|
||
|
||
// User doesn't exist, create a new one
|
||
const newUser = await this.userDbService.createUser({
|
||
mobile: phoneNumber,
|
||
username: phoneNumber,
|
||
otp: "",
|
||
tokens: {
|
||
token: "",
|
||
rfToken: "",
|
||
},
|
||
fullName: "",
|
||
nationalCode: "",
|
||
lastLogin: new Date(),
|
||
clientKey: new Types.ObjectId(),
|
||
birthDay: "",
|
||
city: "",
|
||
address: "",
|
||
state: "",
|
||
otpExpire: 0,
|
||
});
|
||
|
||
return new Types.ObjectId((newUser as any)._id);
|
||
}
|
||
}
|