1
0
forked from Yara724/api

Added refactored version of car-body

This commit is contained in:
SepehrYahyaee
2026-03-14 13:36:05 +03:30
parent 64dfd1ca8a
commit b40270f058
15 changed files with 2786 additions and 1547 deletions

View File

@@ -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, were 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