This commit is contained in:
SepehrYahyaee
2026-04-15 11:43:19 +03:30
parent 0e2789c209
commit f8fbbb7ac6
5 changed files with 131 additions and 55 deletions

View File

@@ -0,0 +1,30 @@
import { ApiProperty } from "@nestjs/swagger";
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
import {
CarBodyFormDto,
DescriptionDto,
InitialFormDto,
} from "./create-request-management.dto";
/**
* V2 (IN_PERSON): single expert description/location for the scene.
*/
export class ExpertCompleteCarBodyFormV2Dto {
@ApiProperty({
description: "First party phone number",
example: "09123456789",
})
firstPartyPhoneNumber: string;
@ApiProperty({ type: InitialFormDto })
firstPartyInitialForm: InitialFormDto;
@ApiProperty({ type: CarBodyFormDto })
carBodyForm: CarBodyFormDto;
@ApiProperty({ type: AddPlateDto })
firstPartyPlate: AddPlateDto;
@ApiProperty({ type: DescriptionDto })
expertDescription: DescriptionDto;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import { LocationDto } from "./create-request-management.dto";
/**
* V2 (IN_PERSON): one scene location provided by expert.
*/
export class ExpertCompleteLocationV2Dto {
@ApiProperty({ type: LocationDto })
location: LocationDto;
}

View File

@@ -0,0 +1,50 @@
import { ApiProperty } from "@nestjs/swagger";
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
import { DescriptionDto, InitialFormDto } from "./create-request-management.dto";
/**
* V2 (IN_PERSON): second party inputs without separate description/location.
*/
export class ExpertSecondPartyInfoV2Dto {
@ApiProperty({
description: "Second party phone number",
example: "09123456789",
})
phoneNumber: string;
@ApiProperty({ type: InitialFormDto })
initialForm: InitialFormDto;
@ApiProperty({ type: AddPlateDto })
plate: AddPlateDto;
}
/**
* V2 (IN_PERSON): single expert description shared for the whole scene.
*/
export class ExpertCompleteThirdPartyFormV2Dto {
@ApiProperty({
description: "First party phone number",
example: "09123456789",
})
firstPartyPhoneNumber: string;
@ApiProperty({ type: InitialFormDto })
firstPartyInitialForm: InitialFormDto;
@ApiProperty({ type: AddPlateDto })
firstPartyPlate: AddPlateDto;
@ApiProperty({ type: ExpertSecondPartyInfoV2Dto })
secondParty: ExpertSecondPartyInfoV2Dto;
@ApiProperty({ type: DescriptionDto })
expertDescription: DescriptionDto;
@ApiProperty({
description:
"Phone number of the guilty party. Must match either first or second party phone number.",
example: "09123456789",
})
guiltyPartyPhoneNumber: string;
}

View File

@@ -27,14 +27,14 @@ import { Roles } from "src/decorators/roles.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
import { RequestManagementService } from "./request-management.service"; import { RequestManagementService } from "./request-management.service";
import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto"; import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto";
import { ExpertCompleteThirdPartyFormDto } from "./dto/expert-complete-third-party-form.dto"; import { ExpertCompleteThirdPartyFormV2Dto } from "./dto/expert-complete-third-party-form.v2.dto";
import { ExpertCompleteCarBodyFormDto } from "./dto/expert-complete-car-body-form.dto"; import { ExpertCompleteCarBodyFormV2Dto } from "./dto/expert-complete-car-body-form.v2.dto";
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto"; import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto"; import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto";
import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signature.dto"; import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signature.dto";
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto"; import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto"; import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
import { ExpertCompleteLocationsDto } from "./dto/expert-complete-locations.dto"; import { ExpertCompleteLocationV2Dto } from "./dto/expert-complete-location.v2.dto";
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service"; import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
import { PartyRole } from "./entities/schema/partyRole.enum"; import { PartyRole } from "./entities/schema/partyRole.enum";
@@ -225,11 +225,11 @@ export class ExpertInitiatedV2Controller {
@ApiOperation({ @ApiOperation({
summary: "[V2] Submit all blame data (IN_PERSON)", summary: "[V2] Submit all blame data (IN_PERSON)",
description: description:
"For IN_PERSON files only. Send THIRD_PARTY or CAR_BODY form EXCEPT locations. After this, call add-locations to submit lat/lon and move workflow to WAITING_FOR_SIGNATURES.", "For IN_PERSON files only. Send THIRD_PARTY or CAR_BODY form EXCEPT location. Use one shared expertDescription (not per-party descriptions). After this, call add-locations once to submit scene lat/lon and move workflow to WAITING_FOR_SIGNATURES.",
}) })
@ApiParam({ name: "requestId", description: "Blame request ID" }) @ApiParam({ name: "requestId", description: "Blame request ID" })
@ApiBody({ @ApiBody({
description: "Choose THIRD_PARTY or CAR_BODY example according to the file type. All nested fields are listed so you can fill or test without guessing property names.", description: "Choose THIRD_PARTY or CAR_BODY example according to the file type. IN_PERSON v2 uses one shared expertDescription for the scene.",
examples: { examples: {
THIRD_PARTY: { THIRD_PARTY: {
summary: "THIRD_PARTY", summary: "THIRD_PARTY",
@@ -253,7 +253,7 @@ export class ExpertInitiatedV2Controller {
insurerBirthday: 13770624, insurerBirthday: 13770624,
driverBirthday: "1370-01-01", driverBirthday: "1370-01-01",
}, },
firstPartyDescription: { desc: "توضیح حادثه طرف اول" }, expertDescription: { desc: "توضیح حادثه کارشناس (مشترک برای صحنه)" },
secondParty: { secondParty: {
phoneNumber: "09187654321", phoneNumber: "09187654321",
initialForm: { initialForm: {
@@ -273,7 +273,6 @@ export class ExpertInitiatedV2Controller {
insurerBirthday: 13700720, insurerBirthday: 13700720,
driverBirthday: "1370-01-01", driverBirthday: "1370-01-01",
}, },
description: { desc: "توضیح حادثه طرف دوم" },
}, },
guiltyPartyPhoneNumber: "09123456789", guiltyPartyPhoneNumber: "09123456789",
}, },
@@ -302,7 +301,7 @@ export class ExpertInitiatedV2Controller {
insurerBirthday: 1370, insurerBirthday: 1370,
driverBirthday: "1370-01-01", driverBirthday: "1370-01-01",
}, },
firstPartyDescription: { expertDescription: {
desc: "توضیح حادثه", desc: "توضیح حادثه",
accidentDate: "2025-01-15", accidentDate: "2025-01-15",
accidentTime: "14:30", accidentTime: "14:30",
@@ -319,7 +318,7 @@ export class ExpertInitiatedV2Controller {
async completeBlameDataV2( async completeBlameDataV2(
@CurrentUser() expert: any, @CurrentUser() expert: any,
@Param("requestId") requestId: string, @Param("requestId") requestId: string,
@Body() formData: any, @Body() formData: ExpertCompleteThirdPartyFormV2Dto | ExpertCompleteCarBodyFormV2Dto,
) { ) {
return this.requestManagementService.expertCompleteBlameDataV2( return this.requestManagementService.expertCompleteBlameDataV2(
expert, expert,
@@ -330,25 +329,18 @@ export class ExpertInitiatedV2Controller {
@Post("add-locations/:requestId") @Post("add-locations/:requestId")
@ApiOperation({ @ApiOperation({
summary: "[V2] Submit location(s) for expert-initiated IN_PERSON blame", summary: "[V2] Submit one scene location for expert-initiated IN_PERSON blame",
description: description:
"Submit first party (and second party for THIRD_PARTY) location after complete-blame-data. This transitions workflow to WAITING_FOR_SIGNATURES.", "Submit one scene location after complete-blame-data. This transitions workflow to WAITING_FOR_SIGNATURES.",
}) })
@ApiParam({ name: "requestId", description: "Blame request ID" }) @ApiParam({ name: "requestId", description: "Blame request ID" })
@ApiBody({ @ApiBody({
type: ExpertCompleteLocationsDto, type: ExpertCompleteLocationV2Dto,
examples: { examples: {
THIRD_PARTY: { scene: {
summary: "THIRD_PARTY locations", summary: "One scene location (all IN_PERSON types)",
value: { value: {
firstPartyLocation: { lat: 35.6892, lon: 51.389 }, location: { lat: 35.6892, lon: 51.389 },
secondPartyLocation: { lat: 35.7001, lon: 51.4102 },
},
},
CAR_BODY: {
summary: "CAR_BODY location",
value: {
firstPartyLocation: { lat: 35.6892, lon: 51.389 },
}, },
}, },
}, },
@@ -357,12 +349,12 @@ export class ExpertInitiatedV2Controller {
async addLocationsV2( async addLocationsV2(
@CurrentUser() expert: any, @CurrentUser() expert: any,
@Param("requestId") requestId: string, @Param("requestId") requestId: string,
@Body() dto: ExpertCompleteLocationsDto, @Body() dto: ExpertCompleteLocationV2Dto,
) { ) {
return this.requestManagementService.expertAddLocationsForBlameV2( return this.requestManagementService.expertAddLocationsForBlameV2(
expert, expert,
requestId, requestId,
dto, dto as any,
); );
} }

View File

@@ -4039,6 +4039,9 @@ export class RequestManagementService {
if (!formData.carBodyForm) { if (!formData.carBodyForm) {
throw new BadRequestException("carBodyForm is required."); throw new BadRequestException("carBodyForm is required.");
} }
if (!formData?.expertDescription?.desc) {
throw new BadRequestException("expertDescription.desc is required.");
}
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber( const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
formData.firstPartyPhoneNumber, formData.firstPartyPhoneNumber,
@@ -4110,12 +4113,12 @@ export class RequestManagementService {
}, },
}, },
statement: { statement: {
description: formData.firstPartyDescription?.desc, description: formData.expertDescription?.desc,
accidentDate: formData.firstPartyDescription?.accidentDate, accidentDate: formData.expertDescription?.accidentDate,
accidentTime: formData.firstPartyDescription?.accidentTime, accidentTime: formData.expertDescription?.accidentTime,
weatherCondition: formData.firstPartyDescription?.weatherCondition, weatherCondition: formData.expertDescription?.weatherCondition,
roadCondition: formData.firstPartyDescription?.roadCondition, roadCondition: formData.expertDescription?.roadCondition,
lightCondition: formData.firstPartyDescription?.lightCondition, lightCondition: formData.expertDescription?.lightCondition,
}, },
location: req.parties?.[0]?.location, location: req.parties?.[0]?.location,
carBodyFirstForm: { carBodyFirstForm: {
@@ -4184,6 +4187,9 @@ export class RequestManagementService {
if (!formData.secondParty || !formData.guiltyPartyPhoneNumber) { if (!formData.secondParty || !formData.guiltyPartyPhoneNumber) {
throw new BadRequestException("secondParty and guiltyPartyPhoneNumber are required."); throw new BadRequestException("secondParty and guiltyPartyPhoneNumber are required.");
} }
if (!formData?.expertDescription?.desc) {
throw new BadRequestException("expertDescription.desc is required.");
}
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber( const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
formData.firstPartyPhoneNumber, formData.firstPartyPhoneNumber,
@@ -4254,7 +4260,7 @@ export class RequestManagementService {
firstPartyUserId, firstPartyUserId,
formData.firstPartyInitialForm, formData.firstPartyInitialForm,
formData.firstPartyPlate, formData.firstPartyPlate,
formData.firstPartyDescription?.desc || "", formData.expertDescription?.desc || "",
PartyRole.FIRST, PartyRole.FIRST,
); );
const secondParty = await buildPartyFromForm( const secondParty = await buildPartyFromForm(
@@ -4262,7 +4268,7 @@ export class RequestManagementService {
secondPartyUserId, secondPartyUserId,
formData.secondParty.initialForm, formData.secondParty.initialForm,
formData.secondParty.plate, formData.secondParty.plate,
formData.secondParty.description?.desc || "", formData.expertDescription?.desc || "",
PartyRole.SECOND, PartyRole.SECOND,
); );
@@ -4320,7 +4326,7 @@ export class RequestManagementService {
async expertAddLocationsForBlameV2( async expertAddLocationsForBlameV2(
expert: any, expert: any,
requestId: string, requestId: string,
dto: { firstPartyLocation: LocationDto; secondPartyLocation?: LocationDto }, dto: { location: LocationDto },
): Promise<any> { ): Promise<any> {
const req = await this.blameRequestDbService.findById(requestId); const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found"); if (!req) throw new NotFoundException("Request not found");
@@ -4336,21 +4342,10 @@ export class RequestManagementService {
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST); const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
if (firstIdx === -1) throw new BadRequestException("First party not found"); if (firstIdx === -1) throw new BadRequestException("First party not found");
if (!dto?.firstPartyLocation) { if (!dto?.location) {
throw new BadRequestException("firstPartyLocation is required"); throw new BadRequestException("location is required");
}
req.parties[firstIdx].location = dto.firstPartyLocation as any;
if (req.type === BlameRequestType.THIRD_PARTY) {
const secondIdx = this.getPartyIndex(req, PartyRole.SECOND);
if (secondIdx === -1) throw new BadRequestException("Second party not found");
if (!dto?.secondPartyLocation) {
throw new BadRequestException(
"secondPartyLocation is required for THIRD_PARTY",
);
}
req.parties[secondIdx].location = dto.secondPartyLocation as any;
} }
req.parties[firstIdx].location = dto.location as any;
const completed = Array.isArray(req.workflow?.completedSteps) const completed = Array.isArray(req.workflow?.completedSteps)
? req.workflow.completedSteps ? req.workflow.completedSteps
@@ -4358,12 +4353,6 @@ export class RequestManagementService {
if (!completed.includes(WorkflowStep.FIRST_LOCATION as any)) { if (!completed.includes(WorkflowStep.FIRST_LOCATION as any)) {
completed.push(WorkflowStep.FIRST_LOCATION as any); completed.push(WorkflowStep.FIRST_LOCATION as any);
} }
if (
req.type === BlameRequestType.THIRD_PARTY &&
!completed.includes(WorkflowStep.SECOND_LOCATION as any)
) {
completed.push(WorkflowStep.SECOND_LOCATION as any);
}
req.workflow = { req.workflow = {
...(req.workflow || {}), ...(req.workflow || {}),
@@ -4383,8 +4372,7 @@ export class RequestManagementService {
actorType: expert?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert", actorType: expert?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert",
}, },
metadata: { metadata: {
hasFirstLocation: true, hasLocation: true,
hasSecondLocation: req.type === BlameRequestType.THIRD_PARTY,
}, },
} as any); } as any);
@@ -4467,6 +4455,12 @@ export class RequestManagementService {
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST); const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
if (firstPartyIndex === -1) throw new BadRequestException("First party not found"); if (firstPartyIndex === -1) throw new BadRequestException("First party not found");
const firstParty = req.parties[firstPartyIndex]; const firstParty = req.parties[firstPartyIndex];
if (
Array.isArray(firstParty?.evidence?.voices) &&
firstParty.evidence.voices.length > 0
) {
throw new ConflictException("Voice already uploaded for this file");
}
const voiceDocument = await this.blameVoiceDbService.create({ const voiceDocument = await this.blameVoiceDbService.create({
fileName: voiceFile.filename, fileName: voiceFile.filename,