forked from Yara724/api
Added refactored version of car-body
This commit is contained in:
@@ -93,6 +93,25 @@ export class RequestManagementService {
|
||||
}
|
||||
|
||||
private async advanceWorkflowToNext(req: any, submittedStepKey: WorkflowStep) {
|
||||
// CAR_BODY: after first-party description there is no second party or expert; go to COMPLETED
|
||||
if (
|
||||
req.type === BlameRequestType.CAR_BODY &&
|
||||
submittedStepKey === WorkflowStep.FIRST_DESCRIPTION
|
||||
) {
|
||||
const completed = Array.isArray(req.workflow.completedSteps)
|
||||
? req.workflow.completedSteps
|
||||
: [];
|
||||
if (!completed.includes(submittedStepKey)) completed.push(submittedStepKey);
|
||||
req.workflow.completedSteps = completed;
|
||||
req.workflow.currentStep = WorkflowStep.COMPLETED;
|
||||
req.workflow.nextStep = undefined;
|
||||
req.status = CaseStatus.COMPLETED;
|
||||
this.logger.debug(
|
||||
"[WORKFLOW] CAR_BODY: advanced to COMPLETED after FIRST_DESCRIPTION",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const submittedStep = await this.getWorkflowStep({ stepKey: submittedStepKey });
|
||||
|
||||
this.logger.debug(
|
||||
@@ -164,6 +183,60 @@ export class RequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures CAR_BODY_ACCIDENT_TYPE step exists in DB (e.g. if initialize was not run after adding CAR_BODY flow).
|
||||
*/
|
||||
private async ensureCarBodyAccidentTypeStepExists(): Promise<void> {
|
||||
const exists = await this.workflowStepDbService.exists(
|
||||
WorkflowStep.CAR_BODY_ACCIDENT_TYPE as any,
|
||||
);
|
||||
if (exists) return;
|
||||
|
||||
this.logger.log(
|
||||
"[WORKFLOW] CAR_BODY_ACCIDENT_TYPE step missing; upserting from default definition",
|
||||
);
|
||||
await this.workflowStepDbService.upsertByStepKey(
|
||||
WorkflowStep.CAR_BODY_ACCIDENT_TYPE as any,
|
||||
{
|
||||
stepKey: WorkflowStep.CAR_BODY_ACCIDENT_TYPE,
|
||||
type: "CAR_BODY",
|
||||
stepNumber: 2,
|
||||
isActive: true,
|
||||
stepName_fa: "نوع حادثه بدنه",
|
||||
stepName_en: "Car Body Accident Type",
|
||||
description_fa: "انتخاب اینکه حادثه با خودرو بوده یا با شیء",
|
||||
description_en:
|
||||
"Select whether accident was with another car or with an object",
|
||||
category: "FIRST_PARTY",
|
||||
requiredPreviousSteps: [WorkflowStep.CREATED],
|
||||
nextPossibleSteps: [WorkflowStep.FIRST_VIDEO],
|
||||
allowedRoles: ["user"],
|
||||
estimatedDuration: 1,
|
||||
fields: [
|
||||
{
|
||||
id: "507f1f77bcf86cd7994390cb" as any,
|
||||
name_fa: "نوع حادثه",
|
||||
name_en: "accidentType",
|
||||
label_fa: "نوع حادثه",
|
||||
label_en: "Accident Type",
|
||||
placeholder_fa: "حادثه با خودرو یا شیء؟",
|
||||
placeholder_en: "Accident with car or object?",
|
||||
value: [
|
||||
{ label_fa: "با خودرو", label_en: "With Car", value: "car" },
|
||||
{ label_fa: "با شیء", label_en: "With Object", value: "object" },
|
||||
],
|
||||
dataType: "select",
|
||||
required: true,
|
||||
order: 1,
|
||||
isActive: true,
|
||||
validation: { enum: ["car", "object"] },
|
||||
},
|
||||
],
|
||||
metadata: { icon: "car", color: "#10B981" },
|
||||
} as any,
|
||||
);
|
||||
}
|
||||
|
||||
private async getWorkflowStep(params: {
|
||||
stepNumber?: number;
|
||||
stepKey?: WorkflowStep;
|
||||
@@ -235,6 +308,12 @@ export class RequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// CAR_BODY skips confession and goes to accident-type form
|
||||
const nextStep =
|
||||
type === BlameRequestType.CAR_BODY
|
||||
? (WorkflowStep.CAR_BODY_ACCIDENT_TYPE as WorkflowStep)
|
||||
: (nextStepFromManager as WorkflowStep);
|
||||
|
||||
const publicId = await this.publicIdService.generateRequestPublicId();
|
||||
|
||||
const created = await this.blameRequestDbService.create({
|
||||
@@ -257,7 +336,7 @@ export class RequestManagementService {
|
||||
],
|
||||
workflow: {
|
||||
currentStep: firstStep.stepKey as WorkflowStep,
|
||||
nextStep: nextStepFromManager as WorkflowStep,
|
||||
nextStep,
|
||||
completedSteps: [firstStep.stepKey as WorkflowStep],
|
||||
locked: false,
|
||||
},
|
||||
@@ -409,6 +488,73 @@ export class RequestManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 CAR_BODY: Submit accident type form (car vs object). Replaces confession step for CAR_BODY.
|
||||
*/
|
||||
async carBodyAccidentTypeFormV2(
|
||||
requestId: string,
|
||||
body: { car?: boolean; object?: boolean },
|
||||
user: any,
|
||||
) {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (req.type !== BlameRequestType.CAR_BODY) {
|
||||
throw new BadRequestException("This endpoint is only for CAR_BODY type requests");
|
||||
}
|
||||
if (!req.workflow?.nextStep) {
|
||||
throw new BadRequestException("Request workflow is not initialized");
|
||||
}
|
||||
if (req.workflow.nextStep !== WorkflowStep.CAR_BODY_ACCIDENT_TYPE) {
|
||||
throw new BadRequestException(
|
||||
`Invalid step. Expected nextStep=CAR_BODY_ACCIDENT_TYPE but got ${req.workflow.nextStep}`,
|
||||
);
|
||||
}
|
||||
|
||||
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstPartyIndex === -1) throw new BadRequestException("First party not found");
|
||||
const firstParty = req.parties[firstPartyIndex];
|
||||
this.assertPartyOwner(firstParty, user, "Only first party can submit this step");
|
||||
|
||||
if (!firstParty.person) firstParty.person = {} as any;
|
||||
if (!firstParty.person.userId && user?.sub) {
|
||||
firstParty.person.userId = Types.ObjectId.isValid(user.sub)
|
||||
? new Types.ObjectId(user.sub)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const car = !!body?.car;
|
||||
const object = !!body?.object;
|
||||
if (!car && !object) {
|
||||
throw new BadRequestException("Set car or object to true");
|
||||
}
|
||||
|
||||
(firstParty as any).carBodyFirstForm = { car, object };
|
||||
req.blameStatus = BlameStatus.AGREED;
|
||||
|
||||
await this.ensureCarBodyAccidentTypeStepExists();
|
||||
await this.advanceWorkflowToNext(req, WorkflowStep.CAR_BODY_ACCIDENT_TYPE);
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "CAR_BODY_ACCIDENT_TYPE_SUBMITTED",
|
||||
actor: {
|
||||
actorId: Types.ObjectId.isValid(user?.sub) ? new Types.ObjectId(user.sub) : undefined,
|
||||
actorName: user?.fullName,
|
||||
actorType: "user",
|
||||
},
|
||||
metadata: { car, object },
|
||||
} as any);
|
||||
|
||||
await (req as any).save();
|
||||
|
||||
return {
|
||||
requestId: req._id,
|
||||
publicId: req.publicId,
|
||||
workflow: req.workflow,
|
||||
blameStatus: req.blameStatus,
|
||||
};
|
||||
}
|
||||
|
||||
async uploadFirstPartyVideoV2(
|
||||
requestId: string,
|
||||
file: Express.Multer.File,
|
||||
@@ -678,6 +824,16 @@ export class RequestManagementService {
|
||||
party.insurance.startDate = inquiryMapped?.IssueDate;
|
||||
party.insurance.endDate = inquiryMapped?.EndDate;
|
||||
|
||||
// CAR_BODY: persist mocked car-body insurance in party's insurance (same place as usual inquiry)
|
||||
if (req.type === BlameRequestType.CAR_BODY && role === PartyRole.FIRST) {
|
||||
const carBodyInfo = await this.mockCarBodyInsuranceInquiry(
|
||||
String(req._id),
|
||||
body.plate,
|
||||
body.nationalCodeOfInsurer,
|
||||
);
|
||||
(party.insurance as any).carBodyInsurance = carBodyInfo;
|
||||
}
|
||||
|
||||
// Advance workflow
|
||||
await this.advanceWorkflowToNext(req, stepKey);
|
||||
|
||||
@@ -881,11 +1037,37 @@ export class RequestManagementService {
|
||||
if (!party.statement) party.statement = {} as any;
|
||||
party.statement.description = body.desc;
|
||||
|
||||
// Optional: map accident date/time to the request-level accident info (if provided)
|
||||
if (body.accidentDate || body.accidentTime) {
|
||||
if (!req.accident) req.accident = {} as any;
|
||||
if (body.accidentDate) req.accident.date = body.accidentDate as any;
|
||||
if (body.accidentTime) req.accident.time = body.accidentTime as any;
|
||||
// THIRD_PARTY: only desc is accepted; reject CAR_BODY-only fields
|
||||
if (req.type === BlameRequestType.THIRD_PARTY) {
|
||||
const hasCarBodyFields =
|
||||
body.accidentDate != null ||
|
||||
body.accidentTime != null ||
|
||||
body.weatherCondition != null ||
|
||||
body.roadCondition != null ||
|
||||
body.lightCondition != null;
|
||||
if (hasCarBodyFields) {
|
||||
throw new BadRequestException(
|
||||
"THIRD_PARTY description step accepts only the desc field. Omit accidentDate, accidentTime, weatherCondition, roadCondition, and lightCondition.",
|
||||
);
|
||||
}
|
||||
} else if (req.type === BlameRequestType.CAR_BODY) {
|
||||
// CAR_BODY: require desc + accident date/time and conditions
|
||||
if (
|
||||
body.accidentDate == null ||
|
||||
body.accidentTime == null ||
|
||||
body.weatherCondition == null ||
|
||||
body.roadCondition == null ||
|
||||
body.lightCondition == null
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"CAR_BODY description step requires desc, accidentDate, accidentTime, weatherCondition, roadCondition, and lightCondition.",
|
||||
);
|
||||
}
|
||||
party.statement.accidentDate = body.accidentDate as any;
|
||||
party.statement.accidentTime = body.accidentTime;
|
||||
(party.statement as any).weatherCondition = body.weatherCondition;
|
||||
(party.statement as any).roadCondition = body.roadCondition;
|
||||
(party.statement as any).lightCondition = body.lightCondition;
|
||||
}
|
||||
|
||||
// If second party finished description, we’re ready for expert flow.
|
||||
@@ -2988,6 +3170,22 @@ export class RequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the current user (field expert) can access this BlameRequest (v2).
|
||||
*/
|
||||
private verifyExpertAccessForBlameV2(req: any, expert: any): void {
|
||||
if (!req?.expertInitiated || !req?.initiatedByFieldExpertId) {
|
||||
throw new ForbiddenException(
|
||||
"This file is not expert-initiated or has no initiating expert",
|
||||
);
|
||||
}
|
||||
if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) {
|
||||
throw new ForbiddenException(
|
||||
"You can only access files that you have initiated",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to add history event to a request
|
||||
*/
|
||||
@@ -3120,6 +3318,151 @@ export class RequestManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Field expert creates an expert-initiated blame file (BlameRequest with workflow).
|
||||
* LINK: creates request with parties identified by phone; expert sends link to user(s) who then fill via normal v2 flow.
|
||||
* IN_PERSON: creates minimal request; expert fills everything via complete-blame-data v2.
|
||||
*/
|
||||
async createExpertInitiatedBlameV2(
|
||||
expert: any,
|
||||
dto: CreateExpertInitiatedFileDto,
|
||||
): Promise<{ requestId: string; publicId: string; linkUrl?: string }> {
|
||||
const expertId = new Types.ObjectId(expert.sub);
|
||||
const type =
|
||||
dto.type === "CAR_BODY"
|
||||
? BlameRequestType.CAR_BODY
|
||||
: BlameRequestType.THIRD_PARTY;
|
||||
|
||||
if (dto.creationMethod === CreationMethod.LINK) {
|
||||
if (!dto.firstPartyPhoneNumber) {
|
||||
throw new BadRequestException(
|
||||
"First party phone number is required for LINK method",
|
||||
);
|
||||
}
|
||||
if (
|
||||
type === BlameRequestType.THIRD_PARTY &&
|
||||
!dto.secondPartyPhoneNumber
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Second party phone number is required for LINK method with THIRD_PARTY type",
|
||||
);
|
||||
}
|
||||
if (
|
||||
type === BlameRequestType.THIRD_PARTY &&
|
||||
dto.firstPartyPhoneNumber === dto.secondPartyPhoneNumber
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"First and second party phone numbers must be different",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const firstStep = await this.getWorkflowStep({ stepNumber: 1 });
|
||||
if (!firstStep?.stepKey) {
|
||||
throw new InternalServerErrorException(
|
||||
"Workflow stepNumber=1 not configured in step manager",
|
||||
);
|
||||
}
|
||||
const nextStepFromManager = firstStep.nextPossibleSteps?.[0];
|
||||
const nextStep =
|
||||
type === BlameRequestType.CAR_BODY
|
||||
? (WorkflowStep.CAR_BODY_ACCIDENT_TYPE as WorkflowStep)
|
||||
: (nextStepFromManager as WorkflowStep);
|
||||
if (!nextStep) {
|
||||
throw new InternalServerErrorException(
|
||||
"Workflow stepNumber=1 has no nextPossibleSteps configured",
|
||||
);
|
||||
}
|
||||
|
||||
const publicId = await this.publicIdService.generateRequestPublicId();
|
||||
|
||||
const parties: any[] = [];
|
||||
|
||||
if (dto.creationMethod === CreationMethod.LINK) {
|
||||
const firstUserId = await this.getOrCreateUserByPhoneNumber(
|
||||
dto.firstPartyPhoneNumber,
|
||||
);
|
||||
parties.push({
|
||||
role: PartyRole.FIRST,
|
||||
person: {
|
||||
userId: firstUserId,
|
||||
phoneNumber: dto.firstPartyPhoneNumber,
|
||||
},
|
||||
});
|
||||
if (
|
||||
type === BlameRequestType.THIRD_PARTY &&
|
||||
dto.secondPartyPhoneNumber
|
||||
) {
|
||||
const secondUserId = await this.getOrCreateUserByPhoneNumber(
|
||||
dto.secondPartyPhoneNumber,
|
||||
);
|
||||
parties.push({
|
||||
role: PartyRole.SECOND,
|
||||
person: {
|
||||
userId: secondUserId,
|
||||
phoneNumber: dto.secondPartyPhoneNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
parties.push({
|
||||
role: PartyRole.FIRST,
|
||||
person: {},
|
||||
});
|
||||
}
|
||||
|
||||
const created = await this.blameRequestDbService.create({
|
||||
publicId,
|
||||
requestNo: publicId,
|
||||
type,
|
||||
parties,
|
||||
workflow: {
|
||||
currentStep: firstStep.stepKey as WorkflowStep,
|
||||
nextStep,
|
||||
completedSteps: [firstStep.stepKey as WorkflowStep],
|
||||
locked: false,
|
||||
},
|
||||
history: [],
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertId,
|
||||
creationMethod: dto.creationMethod,
|
||||
filledBy:
|
||||
dto.creationMethod === CreationMethod.IN_PERSON
|
||||
? FilledBy.EXPERT
|
||||
: FilledBy.CUSTOMER,
|
||||
});
|
||||
|
||||
const requestId = String((created as any)._id);
|
||||
if (!Array.isArray((created as any).history)) (created as any).history = [];
|
||||
(created as any).history.push({
|
||||
type: "FILE_CREATED_BY_FIELD_EXPERT",
|
||||
actor: {
|
||||
actorId: expertId,
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
},
|
||||
metadata: { creationMethod: dto.creationMethod, type: dto.type },
|
||||
});
|
||||
await (created as any).save();
|
||||
|
||||
const result: {
|
||||
requestId: string;
|
||||
publicId: string;
|
||||
linkUrl?: string;
|
||||
} = {
|
||||
requestId,
|
||||
publicId: (created as any).publicId,
|
||||
};
|
||||
if (dto.creationMethod === CreationMethod.LINK) {
|
||||
const baseUrl =
|
||||
process.env.FRONTEND_BASE_URL || process.env.APP_URL || "";
|
||||
result.linkUrl = baseUrl
|
||||
? `${baseUrl.replace(/\/$/, "")}/blame/${requestId}`
|
||||
: undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all expert-initiated blame files for the current field expert (their panel).
|
||||
* Only files where initiatedBy === current user are returned.
|
||||
@@ -3143,6 +3486,72 @@ export class RequestManagementService {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: List expert-initiated blame files (BlameRequest) for the current field expert.
|
||||
* Only files where initiatedByFieldExpertId === current expert are returned.
|
||||
*/
|
||||
async getMyExpertInitiatedFilesV2(expert: any): Promise<any[]> {
|
||||
const expertId = new Types.ObjectId(expert.sub);
|
||||
const files = await this.blameRequestDbService.find({
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertId,
|
||||
});
|
||||
return (files || []).map((f: any) => ({
|
||||
_id: f._id,
|
||||
publicId: f.publicId,
|
||||
requestNo: f.requestNo,
|
||||
type: f.type,
|
||||
creationMethod: f.creationMethod,
|
||||
filledBy: f.filledBy,
|
||||
status: f.status,
|
||||
blameStatus: f.blameStatus,
|
||||
workflow: f.workflow,
|
||||
partiesCount: Array.isArray(f.parties) ? f.parties.length : 0,
|
||||
createdAt: f.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Get one blame request by id. Access allowed if current user is a party (by userId or phone)
|
||||
* or the initiating field expert. For expert-initiated LINK, party access by phone is sufficient.
|
||||
*/
|
||||
async getBlameRequestV2(requestId: string, user: any): Promise<any> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
const isFieldExpertOwner =
|
||||
req.expertInitiated &&
|
||||
req.initiatedByFieldExpertId &&
|
||||
String(req.initiatedByFieldExpertId) === String(user?.sub);
|
||||
const isParty = Array.isArray(req.parties) && req.parties.some((p: any) => {
|
||||
const pid = p?.person?.userId ? String(p.person.userId) : null;
|
||||
const phone = p?.person?.phoneNumber;
|
||||
return (pid && pid === String(user?.sub)) || (phone && phone === user?.username);
|
||||
});
|
||||
if (!isFieldExpertOwner && !isParty) {
|
||||
throw new ForbiddenException("You do not have access to this request");
|
||||
}
|
||||
// Optionally bind userId to party when user opens via LINK (phone match, no userId yet)
|
||||
if (!isFieldExpertOwner && user?.sub && Types.ObjectId.isValid(user.sub)) {
|
||||
let updated = false;
|
||||
for (const p of req.parties || []) {
|
||||
if (
|
||||
p?.person?.phoneNumber === user?.username &&
|
||||
!p.person?.userId
|
||||
) {
|
||||
p.person = p.person || {};
|
||||
(p.person as any).userId = new Types.ObjectId(user.sub);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
await (req as any).save();
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single endpoint handler: complete all blame-needed data.
|
||||
* Routes to THIRD_PARTY or CAR_BODY completion based on request type.
|
||||
@@ -3174,6 +3583,459 @@ export class RequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert completes blame data for IN_PERSON BlameRequest.
|
||||
* Delegates to expertCompleteThirdPartyFormV2 or expertCompleteCarBodyFormV2.
|
||||
*/
|
||||
async expertCompleteBlameDataV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
formData: any,
|
||||
): Promise<any> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON BlameRequest files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (req.type === BlameRequestType.THIRD_PARTY) {
|
||||
return this.expertCompleteThirdPartyFormV2(expert, requestId, formData);
|
||||
}
|
||||
if (req.type === BlameRequestType.CAR_BODY) {
|
||||
return this.expertCompleteCarBodyFormV2(expert, requestId, formData);
|
||||
}
|
||||
throw new BadRequestException(
|
||||
"Unknown file type. Must be THIRD_PARTY or CAR_BODY.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert completes IN_PERSON CAR_BODY BlameRequest in one go.
|
||||
*/
|
||||
async expertCompleteCarBodyFormV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
formData: any,
|
||||
): Promise<any> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new BadRequestException("Only expert-initiated IN_PERSON CAR_BODY files.");
|
||||
}
|
||||
if (req.type !== BlameRequestType.CAR_BODY) {
|
||||
throw new BadRequestException("File type is not CAR_BODY.");
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!formData.carBodyForm) {
|
||||
throw new BadRequestException("carBodyForm is required.");
|
||||
}
|
||||
|
||||
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||||
formData.firstPartyPhoneNumber,
|
||||
);
|
||||
|
||||
const firstPartyPlate = formData.firstPartyPlate;
|
||||
let sandHubReport: any;
|
||||
try {
|
||||
const sandHubResponse = await this.sandHubService.getSandHubResponse({
|
||||
plate: firstPartyPlate.plate,
|
||||
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
||||
});
|
||||
sandHubReport = sandHubResponse["_doc"] || sandHubResponse;
|
||||
} catch (e) {
|
||||
this.logger.error("SandHub error in expertCompleteCarBodyFormV2:", e);
|
||||
throw new InternalServerErrorException("Failed to process plate information.");
|
||||
}
|
||||
|
||||
const clientName = sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
|
||||
const companyCode = sandHubReport?.CompanyCode;
|
||||
const client = companyCode
|
||||
? await this.clientService.findClientWithCompanyCode(+companyCode)
|
||||
: await this.clientService.findOne({ clientName });
|
||||
if (!client) {
|
||||
throw new NotFoundException(`Client not found for company: ${clientName}`);
|
||||
}
|
||||
|
||||
const carBodyInfo = await this.mockCarBodyInsuranceInquiry(
|
||||
requestId,
|
||||
firstPartyPlate.plate,
|
||||
firstPartyPlate.nationalCodeOfInsurer,
|
||||
);
|
||||
|
||||
const firstParty: any = {
|
||||
role: PartyRole.FIRST,
|
||||
person: {
|
||||
userId: firstPartyUserId,
|
||||
phoneNumber: formData.firstPartyPhoneNumber,
|
||||
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
||||
nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver,
|
||||
insurerLicense: firstPartyPlate.insurerLicense,
|
||||
driverLicense: firstPartyPlate.driverLicense,
|
||||
driverIsInsurer: firstPartyPlate.driverIsInsurer,
|
||||
isNewCar: firstPartyPlate.isNewCar,
|
||||
userNoCertificate: firstPartyPlate.userNoCertificate,
|
||||
insurerBirthday: firstPartyPlate.insurerBirthday,
|
||||
driverBirthday: firstPartyPlate.driverBirthday,
|
||||
},
|
||||
vehicle: {
|
||||
plateId: firstPartyPlate.plate,
|
||||
name: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||||
model: sandHubReport?.MapTypNam,
|
||||
type: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||||
isNew: firstPartyPlate.isNewCar,
|
||||
inquiry: sandHubReport,
|
||||
},
|
||||
insurance: {
|
||||
policyNumber: sandHubReport?.LastCompanyDocumentNumber || sandHubReport?.DocumentNumber,
|
||||
company: clientName,
|
||||
startDate: sandHubReport?.IssueDate || sandHubReport?.StartDate,
|
||||
endDate: sandHubReport?.EndDate,
|
||||
financialCeiling: sandHubReport?.FinancialCvrCptl || sandHubReport?.FinancialCommitment,
|
||||
carBodyInsurance: {
|
||||
policyNumber: carBodyInfo.policyNumber,
|
||||
startDate: carBodyInfo.startDate,
|
||||
endDate: carBodyInfo.endDate,
|
||||
insurerCompany: carBodyInfo.insurerCompany,
|
||||
coverages: carBodyInfo.coverages,
|
||||
},
|
||||
},
|
||||
statement: {
|
||||
description: formData.firstPartyDescription?.desc,
|
||||
accidentDate: formData.firstPartyDescription?.accidentDate,
|
||||
accidentTime: formData.firstPartyDescription?.accidentTime,
|
||||
weatherCondition: formData.firstPartyDescription?.weatherCondition,
|
||||
roadCondition: formData.firstPartyDescription?.roadCondition,
|
||||
lightCondition: formData.firstPartyDescription?.lightCondition,
|
||||
},
|
||||
location: formData.firstPartyLocation,
|
||||
carBodyFirstForm: {
|
||||
car: !!formData.carBodyForm.car,
|
||||
object: !!formData.carBodyForm.object,
|
||||
},
|
||||
evidence: req.parties?.[0]?.evidence ? { ...req.parties[0].evidence } : {},
|
||||
};
|
||||
|
||||
req.parties = [firstParty];
|
||||
req.workflow = {
|
||||
currentStep: WorkflowStep.COMPLETED,
|
||||
nextStep: undefined,
|
||||
completedSteps: [
|
||||
WorkflowStep.CREATED,
|
||||
WorkflowStep.CAR_BODY_ACCIDENT_TYPE,
|
||||
WorkflowStep.FIRST_VIDEO,
|
||||
WorkflowStep.FIRST_INITIAL_FORM,
|
||||
WorkflowStep.FIRST_LOCATION,
|
||||
WorkflowStep.FIRST_VOICE,
|
||||
WorkflowStep.FIRST_DESCRIPTION,
|
||||
].filter((s) => s),
|
||||
locked: false,
|
||||
};
|
||||
req.status = CaseStatus.COMPLETED;
|
||||
req.blameStatus = BlameStatus.AGREED;
|
||||
(req as any).filledBy = FilledBy.EXPERT;
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "EXPERT_COMPLETED_CAR_BODY_FORM_V2",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
},
|
||||
metadata: {},
|
||||
} as any);
|
||||
await (req as any).save();
|
||||
|
||||
return {
|
||||
requestId: req._id,
|
||||
publicId: req.publicId,
|
||||
workflow: req.workflow,
|
||||
status: req.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert completes IN_PERSON THIRD_PARTY BlameRequest in one go.
|
||||
*/
|
||||
async expertCompleteThirdPartyFormV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
formData: any,
|
||||
): Promise<any> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new BadRequestException("Only expert-initiated IN_PERSON THIRD_PARTY files.");
|
||||
}
|
||||
if (req.type !== BlameRequestType.THIRD_PARTY) {
|
||||
throw new BadRequestException("File type is not THIRD_PARTY.");
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!formData.secondParty || !formData.guiltyPartyPhoneNumber) {
|
||||
throw new BadRequestException("secondParty and guiltyPartyPhoneNumber are required.");
|
||||
}
|
||||
|
||||
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||||
formData.firstPartyPhoneNumber,
|
||||
);
|
||||
const secondPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||||
formData.secondParty.phoneNumber,
|
||||
);
|
||||
|
||||
const buildPartyFromForm = async (
|
||||
phoneNumber: string,
|
||||
userId: Types.ObjectId,
|
||||
initialForm: any,
|
||||
plateDto: any,
|
||||
locationDto: any,
|
||||
desc: string,
|
||||
role: PartyRole,
|
||||
) => {
|
||||
const sandHubResponse = await this.sandHubService.getSandHubResponse({
|
||||
plate: plateDto.plate,
|
||||
nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer,
|
||||
});
|
||||
const sandHubReport = (sandHubResponse["_doc"] || sandHubResponse) as any;
|
||||
const clientName = sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
|
||||
const companyCode = sandHubReport?.CompanyCode;
|
||||
const client = companyCode
|
||||
? await this.clientService.findClientWithCompanyCode(+companyCode)
|
||||
: await this.clientService.findOne({ clientName });
|
||||
if (!client) {
|
||||
throw new NotFoundException(`Client not found for company: ${clientName}`);
|
||||
}
|
||||
return {
|
||||
role,
|
||||
person: {
|
||||
userId,
|
||||
phoneNumber,
|
||||
nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer,
|
||||
nationalCodeOfDriver: plateDto.nationalCodeOfDriver,
|
||||
insurerLicense: plateDto.insurerLicense,
|
||||
driverLicense: plateDto.driverLicense,
|
||||
driverIsInsurer: plateDto.driverIsInsurer,
|
||||
isNewCar: plateDto.isNewCar,
|
||||
userNoCertificate: plateDto.userNoCertificate,
|
||||
insurerBirthday: plateDto.insurerBirthday,
|
||||
driverBirthday: plateDto.driverBirthday,
|
||||
},
|
||||
vehicle: {
|
||||
plateId: plateDto.plate,
|
||||
name: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||||
model: sandHubReport?.MapTypNam,
|
||||
type: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||||
isNew: plateDto.isNewCar,
|
||||
inquiry: sandHubReport,
|
||||
},
|
||||
insurance: {
|
||||
policyNumber: sandHubReport?.LastCompanyDocumentNumber || sandHubReport?.DocumentNumber,
|
||||
company: clientName,
|
||||
startDate: sandHubReport?.IssueDate || sandHubReport?.StartDate,
|
||||
endDate: sandHubReport?.EndDate,
|
||||
financialCeiling: sandHubReport?.FinancialCvrCptl || sandHubReport?.FinancialCommitment,
|
||||
},
|
||||
statement: { description: desc },
|
||||
location: locationDto,
|
||||
evidence: {},
|
||||
};
|
||||
};
|
||||
|
||||
const firstParty = await buildPartyFromForm(
|
||||
formData.firstPartyPhoneNumber,
|
||||
firstPartyUserId,
|
||||
formData.firstPartyInitialForm,
|
||||
formData.firstPartyPlate,
|
||||
formData.firstPartyLocation,
|
||||
formData.firstPartyDescription?.desc || "",
|
||||
PartyRole.FIRST,
|
||||
);
|
||||
const secondParty = await buildPartyFromForm(
|
||||
formData.secondParty.phoneNumber,
|
||||
secondPartyUserId,
|
||||
formData.secondParty.initialForm,
|
||||
formData.secondParty.plate,
|
||||
formData.secondParty.location,
|
||||
formData.secondParty.description?.desc || "",
|
||||
PartyRole.SECOND,
|
||||
);
|
||||
|
||||
req.parties = [firstParty, secondParty];
|
||||
req.workflow = {
|
||||
currentStep: WorkflowStep.COMPLETED,
|
||||
nextStep: undefined,
|
||||
completedSteps: [
|
||||
WorkflowStep.CREATED,
|
||||
WorkflowStep.FIRST_BLAME_CONFESSION,
|
||||
WorkflowStep.FIRST_VIDEO,
|
||||
WorkflowStep.FIRST_INITIAL_FORM,
|
||||
WorkflowStep.FIRST_LOCATION,
|
||||
WorkflowStep.FIRST_VOICE,
|
||||
WorkflowStep.FIRST_DESCRIPTION,
|
||||
WorkflowStep.SECOND_INITIAL_FORM,
|
||||
WorkflowStep.SECOND_LOCATION,
|
||||
WorkflowStep.SECOND_VOICE,
|
||||
WorkflowStep.SECOND_DESCRIPTION,
|
||||
].filter((s) => s),
|
||||
locked: false,
|
||||
};
|
||||
req.status = CaseStatus.COMPLETED;
|
||||
req.blameStatus = BlameStatus.AGREED;
|
||||
(req as any).filledBy = FilledBy.EXPERT;
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "EXPERT_COMPLETED_THIRD_PARTY_FORM_V2",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
},
|
||||
metadata: { guiltyPartyPhoneNumber: formData.guiltyPartyPhoneNumber },
|
||||
} as any);
|
||||
await (req as any).save();
|
||||
|
||||
return {
|
||||
requestId: req._id,
|
||||
publicId: req.publicId,
|
||||
workflow: req.workflow,
|
||||
status: req.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert uploads video for expert-initiated BlameRequest (first party).
|
||||
*/
|
||||
async expertUploadVideoForBlameV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
file: Express.Multer.File,
|
||||
): Promise<any> {
|
||||
if (!file) throw new BadRequestException("Video file is required");
|
||||
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated) {
|
||||
throw new BadRequestException("This endpoint is only for expert-initiated files.");
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstPartyIndex === -1) throw new BadRequestException("First party not found");
|
||||
const firstParty = req.parties[firstPartyIndex];
|
||||
|
||||
if (firstParty?.evidence?.videoId) {
|
||||
throw new ConflictException("Video already uploaded for this file");
|
||||
}
|
||||
|
||||
const videoDocument = await this.blameVideoDbService.create({
|
||||
fileName: file.filename,
|
||||
path: file.path,
|
||||
requestId: new Types.ObjectId(requestId),
|
||||
} as any);
|
||||
|
||||
if (!firstParty.evidence) firstParty.evidence = {} as any;
|
||||
firstParty.evidence.videoId = String((videoDocument as any)._id);
|
||||
await (req as any).save();
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "EXPERT_UPLOADED_VIDEO_V2",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
},
|
||||
metadata: { videoId: firstParty.evidence.videoId },
|
||||
} as any);
|
||||
await (req as any).save();
|
||||
|
||||
return { requestId: req._id, videoId: firstParty.evidence.videoId };
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert uploads voice for expert-initiated BlameRequest (first party).
|
||||
*/
|
||||
async expertUploadVoiceForBlameV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
voiceFile: Express.Multer.File,
|
||||
): Promise<any> {
|
||||
if (!voiceFile) throw new BadRequestException("Voice file is required");
|
||||
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated) {
|
||||
throw new BadRequestException("This endpoint is only for expert-initiated files.");
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstPartyIndex === -1) throw new BadRequestException("First party not found");
|
||||
const firstParty = req.parties[firstPartyIndex];
|
||||
|
||||
const voiceDocument = await this.blameVoiceDbService.create({
|
||||
fileName: voiceFile.filename,
|
||||
path: voiceFile.path,
|
||||
requestId: new Types.ObjectId(requestId),
|
||||
} as any);
|
||||
|
||||
if (!firstParty.evidence) firstParty.evidence = {} as any;
|
||||
if (!Array.isArray(firstParty.evidence.voices)) firstParty.evidence.voices = [];
|
||||
firstParty.evidence.voices.push(String((voiceDocument as any)._id));
|
||||
await (req as any).save();
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "EXPERT_UPLOADED_VOICE_V2",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
},
|
||||
metadata: {},
|
||||
} as any);
|
||||
await (req as any).save();
|
||||
|
||||
return { requestId: req._id };
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert adds accident fields to expert-initiated BlameRequest.
|
||||
*/
|
||||
async expertAddAccidentFieldsForBlameV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
fields: any,
|
||||
): Promise<any> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated) {
|
||||
throw new BadRequestException("This endpoint is only for expert-initiated files.");
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!req.expert) req.expert = {} as any;
|
||||
if (!req.expert.decision) req.expert.decision = {} as any;
|
||||
(req.expert as any).decision = {
|
||||
...(req.expert as any).decision,
|
||||
fields: {
|
||||
accidentWay: fields.accidentWay,
|
||||
accidentReason: fields.accidentReason,
|
||||
accidentType: fields.accidentType,
|
||||
},
|
||||
};
|
||||
await (req as any).save();
|
||||
|
||||
return { requestId: req._id };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
Reference in New Issue
Block a user