diff --git a/src/request-management/dto/expert-complete-car-body-form.v2.dto.ts b/src/request-management/dto/expert-complete-car-body-form.v2.dto.ts new file mode 100644 index 0000000..f49a237 --- /dev/null +++ b/src/request-management/dto/expert-complete-car-body-form.v2.dto.ts @@ -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; +} diff --git a/src/request-management/dto/expert-complete-location.v2.dto.ts b/src/request-management/dto/expert-complete-location.v2.dto.ts new file mode 100644 index 0000000..69d5a52 --- /dev/null +++ b/src/request-management/dto/expert-complete-location.v2.dto.ts @@ -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; +} diff --git a/src/request-management/dto/expert-complete-third-party-form.v2.dto.ts b/src/request-management/dto/expert-complete-third-party-form.v2.dto.ts new file mode 100644 index 0000000..d1461ee --- /dev/null +++ b/src/request-management/dto/expert-complete-third-party-form.v2.dto.ts @@ -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; +} diff --git a/src/request-management/expert-initiated.v2.controller.ts b/src/request-management/expert-initiated.v2.controller.ts index 060915b..5b6b169 100644 --- a/src/request-management/expert-initiated.v2.controller.ts +++ b/src/request-management/expert-initiated.v2.controller.ts @@ -27,14 +27,14 @@ import { Roles } from "src/decorators/roles.decorator"; import { RoleEnum } from "src/Types&Enums/role.enum"; import { RequestManagementService } from "./request-management.service"; import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto"; -import { ExpertCompleteThirdPartyFormDto } from "./dto/expert-complete-third-party-form.dto"; -import { ExpertCompleteCarBodyFormDto } from "./dto/expert-complete-car-body-form.dto"; +import { ExpertCompleteThirdPartyFormV2Dto } from "./dto/expert-complete-third-party-form.v2.dto"; +import { ExpertCompleteCarBodyFormV2Dto } from "./dto/expert-complete-car-body-form.v2.dto"; import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto"; import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto"; import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signature.dto"; import { VerifyPartyOtpsDto } from "./dto/verify-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 { PartyRole } from "./entities/schema/partyRole.enum"; @@ -225,11 +225,11 @@ export class ExpertInitiatedV2Controller { @ApiOperation({ summary: "[V2] Submit all blame data (IN_PERSON)", 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" }) @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: { THIRD_PARTY: { summary: "THIRD_PARTY", @@ -253,7 +253,7 @@ export class ExpertInitiatedV2Controller { insurerBirthday: 13770624, driverBirthday: "1370-01-01", }, - firstPartyDescription: { desc: "توضیح حادثه طرف اول" }, + expertDescription: { desc: "توضیح حادثه کارشناس (مشترک برای صحنه)" }, secondParty: { phoneNumber: "09187654321", initialForm: { @@ -273,7 +273,6 @@ export class ExpertInitiatedV2Controller { insurerBirthday: 13700720, driverBirthday: "1370-01-01", }, - description: { desc: "توضیح حادثه طرف دوم" }, }, guiltyPartyPhoneNumber: "09123456789", }, @@ -302,7 +301,7 @@ export class ExpertInitiatedV2Controller { insurerBirthday: 1370, driverBirthday: "1370-01-01", }, - firstPartyDescription: { + expertDescription: { desc: "توضیح حادثه", accidentDate: "2025-01-15", accidentTime: "14:30", @@ -319,7 +318,7 @@ export class ExpertInitiatedV2Controller { async completeBlameDataV2( @CurrentUser() expert: any, @Param("requestId") requestId: string, - @Body() formData: any, + @Body() formData: ExpertCompleteThirdPartyFormV2Dto | ExpertCompleteCarBodyFormV2Dto, ) { return this.requestManagementService.expertCompleteBlameDataV2( expert, @@ -330,25 +329,18 @@ export class ExpertInitiatedV2Controller { @Post("add-locations/:requestId") @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: - "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" }) @ApiBody({ - type: ExpertCompleteLocationsDto, + type: ExpertCompleteLocationV2Dto, examples: { - THIRD_PARTY: { - summary: "THIRD_PARTY locations", + scene: { + summary: "One scene location (all IN_PERSON types)", value: { - firstPartyLocation: { 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 }, + location: { lat: 35.6892, lon: 51.389 }, }, }, }, @@ -357,12 +349,12 @@ export class ExpertInitiatedV2Controller { async addLocationsV2( @CurrentUser() expert: any, @Param("requestId") requestId: string, - @Body() dto: ExpertCompleteLocationsDto, + @Body() dto: ExpertCompleteLocationV2Dto, ) { return this.requestManagementService.expertAddLocationsForBlameV2( expert, requestId, - dto, + dto as any, ); } diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 5bdd838..17270fc 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -4039,6 +4039,9 @@ export class RequestManagementService { if (!formData.carBodyForm) { throw new BadRequestException("carBodyForm is required."); } + if (!formData?.expertDescription?.desc) { + throw new BadRequestException("expertDescription.desc is required."); + } const firstPartyUserId = await this.getOrCreateUserByPhoneNumber( formData.firstPartyPhoneNumber, @@ -4110,12 +4113,12 @@ export class RequestManagementService { }, }, 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, + description: formData.expertDescription?.desc, + accidentDate: formData.expertDescription?.accidentDate, + accidentTime: formData.expertDescription?.accidentTime, + weatherCondition: formData.expertDescription?.weatherCondition, + roadCondition: formData.expertDescription?.roadCondition, + lightCondition: formData.expertDescription?.lightCondition, }, location: req.parties?.[0]?.location, carBodyFirstForm: { @@ -4184,6 +4187,9 @@ export class RequestManagementService { if (!formData.secondParty || !formData.guiltyPartyPhoneNumber) { throw new BadRequestException("secondParty and guiltyPartyPhoneNumber are required."); } + if (!formData?.expertDescription?.desc) { + throw new BadRequestException("expertDescription.desc is required."); + } const firstPartyUserId = await this.getOrCreateUserByPhoneNumber( formData.firstPartyPhoneNumber, @@ -4254,7 +4260,7 @@ export class RequestManagementService { firstPartyUserId, formData.firstPartyInitialForm, formData.firstPartyPlate, - formData.firstPartyDescription?.desc || "", + formData.expertDescription?.desc || "", PartyRole.FIRST, ); const secondParty = await buildPartyFromForm( @@ -4262,7 +4268,7 @@ export class RequestManagementService { secondPartyUserId, formData.secondParty.initialForm, formData.secondParty.plate, - formData.secondParty.description?.desc || "", + formData.expertDescription?.desc || "", PartyRole.SECOND, ); @@ -4320,7 +4326,7 @@ export class RequestManagementService { async expertAddLocationsForBlameV2( expert: any, requestId: string, - dto: { firstPartyLocation: LocationDto; secondPartyLocation?: LocationDto }, + dto: { location: LocationDto }, ): Promise { const req = await this.blameRequestDbService.findById(requestId); if (!req) throw new NotFoundException("Request not found"); @@ -4336,21 +4342,10 @@ export class RequestManagementService { const firstIdx = this.getPartyIndex(req, PartyRole.FIRST); if (firstIdx === -1) throw new BadRequestException("First party not found"); - if (!dto?.firstPartyLocation) { - throw new BadRequestException("firstPartyLocation 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; + if (!dto?.location) { + throw new BadRequestException("location is required"); } + req.parties[firstIdx].location = dto.location as any; const completed = Array.isArray(req.workflow?.completedSteps) ? req.workflow.completedSteps @@ -4358,12 +4353,6 @@ export class RequestManagementService { if (!completed.includes(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 || {}), @@ -4383,8 +4372,7 @@ export class RequestManagementService { actorType: expert?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert", }, metadata: { - hasFirstLocation: true, - hasSecondLocation: req.type === BlameRequestType.THIRD_PARTY, + hasLocation: true, }, } as any); @@ -4467,6 +4455,12 @@ export class RequestManagementService { const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST); if (firstPartyIndex === -1) throw new BadRequestException("First party not found"); 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({ fileName: voiceFile.filename,