From 0e2789c209970217c99f6f574e269e3ba6f4409f Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 15 Apr 2026 10:16:18 +0330 Subject: [PATCH 1/3] YARA-789 --- .../claim-request-management.service.ts | 351 +++++++++++++++++- 1 file changed, 349 insertions(+), 2 deletions(-) diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 111e1e6..7f1225f 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -63,7 +63,9 @@ import { PublicIdService } from "src/utils/public-id/public-id.service"; import { ImageRequiredModel } from "./entites/schema/image-required.schema"; import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service"; import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum"; -import { BranchDbService } from "src/client/entities/db-service/branch.db.service";import { RoleEnum } from "src/Types&Enums/role.enum"; +import { BranchDbService } from "src/client/entities/db-service/branch.db.service"; +import { CreationMethod } from "src/request-management/entities/schema/request-management.schema"; +import { RoleEnum } from "src/Types&Enums/role.enum"; import { UserRatingDto } from "./dto/user-rating.dto"; @Injectable() @@ -336,6 +338,166 @@ export class ClaimRequestManagementService { return trueItems; } + /** One section of CarDamagePartDto: either a single object or an array of objects (legacy). */ + private carPartDamageSectionToRows(section: unknown): Record[] { + if (section == null) return []; + if (Array.isArray(section)) { + return section.filter( + (row): row is Record => + !!row && typeof row === "object", + ); + } + if (typeof section === "object") { + return [section as Record]; + } + return []; + } + + private carPartDamageSectionHasTrue(section: unknown): boolean { + for (const row of this.carPartDamageSectionToRows(section)) { + for (const val of Object.values(row)) { + if (val === true) return true; + } + } + return false; + } + + /** + * At most two of left, right, front, back may contain any damaged part (true). + * `top` and claim `otherParts` are not subject to this rule. + */ + private assertCarPartDamageAtMostTwoOfFourSides(dto: CarDamagePartDto): void { + const d = dto as unknown as Record; + const quadrants = [ + { key: "left", section: d.left }, + { key: "right", section: d.right }, + { key: "front", section: d.front }, + { key: "back", section: d.back }, + ]; + const used = quadrants.filter((q) => + this.carPartDamageSectionHasTrue(q.section), + ); + if (used.length > 2) { + throw new BadRequestException( + `At most two of the four sides (left, right, front, back) may include damaged parts. ` + + `Sides with selections: ${used.map((u) => u.key).join(", ")}.`, + ); + } + } + + /** + * Maps legacy expert CarDamagePartDto (nested booleans) to v2 outer part slugs (ClaimCase.damage.selectedParts). + */ + private carDamagePartDtoToOuterPartSlugs(dto: CarDamagePartDto): string[] { + const out = new Set(); + const add = (slug: string | undefined) => { + if (slug) out.add(slug); + }; + const walkMainOnSide = ( + side: "left" | "right", + items: unknown, + map: Record, + ) => { + for (const row of this.carPartDamageSectionToRows(items)) { + for (const [key, val] of Object.entries(row)) { + if (val !== true) continue; + const slug = map[`${side}_${key}`] ?? map[key]; + add(slug); + } + } + }; + // MainParts on left / right (side-specific door / fender) + walkMainOnSide("left", (dto as any).left, { + left_frontDoor: "front_left_door", + left_backDoor: "rear_left_door", + left_frontFender: "front_left_fender", + left_backFender: "rear_left_fender", + frontDoor: "front_left_door", + backDoor: "rear_left_door", + frontFender: "front_left_fender", + backFender: "rear_left_fender", + }); + walkMainOnSide("right", (dto as any).right, { + right_frontDoor: "front_right_door", + right_backDoor: "rear_right_door", + right_frontFender: "front_right_fender", + right_backFender: "rear_right_fender", + frontDoor: "front_right_door", + backDoor: "rear_right_door", + frontFender: "front_right_fender", + backFender: "rear_right_fender", + }); + const walkSimple = (items: unknown, keyToSlug: Record) => { + for (const row of this.carPartDamageSectionToRows(items)) { + for (const [key, val] of Object.entries(row)) { + if (val === true) add(keyToSlug[key]); + } + } + }; + walkSimple((dto as any).front, { + frontBumper: "front_bumper", + carHood: "hood", + }); + walkSimple((dto as any).back, { + backBumper: "rear_bumper", + carTrunk: "trunk", + }); + walkSimple((dto as any).top, { + roof: "roof", + }); + return Array.from(out); + } + + /** Normalize expert DTO otherParts (strings or legacy { label: true }[]) to string[]. */ + private normalizeExpertOtherPartsInput(raw: any[] | undefined): string[] { + if (!raw || !Array.isArray(raw)) return []; + const out: string[] = []; + for (const item of raw) { + if (typeof item === "string" && item.trim()) { + out.push(item.trim()); + continue; + } + if (item && typeof item === "object") { + for (const [k, v] of Object.entries(item)) { + if (v === true && k) out.push(k); + } + } + } + return out; + } + + private async assertFieldExpertInPersonBlameForClaimCase( + claimCase: any, + expert: any, + ): Promise { + const blameRequestId = claimCase?.blameRequestId?.toString?.(); + if (!blameRequestId) { + throw new BadRequestException("Claim case has no linked blame request."); + } + const blameRequest = await this.blameRequestDbService.findById(blameRequestId); + if (!blameRequest) { + throw new NotFoundException("Blame request not found for this claim."); + } + if ( + !blameRequest.expertInitiated || + blameRequest.creationMethod !== CreationMethod.IN_PERSON + ) { + throw new BadRequestException( + "This endpoint is only for expert-initiated IN_PERSON claim files.", + ); + } + if (!blameRequest.initiatedByFieldExpertId) { + throw new BadRequestException( + "Blame request is missing initiating field expert.", + ); + } + if (String(blameRequest.initiatedByFieldExpertId) !== String(expert.sub)) { + throw new ForbiddenException( + "Only the initiating field expert can complete claim data for this file.", + ); + } + } + async setCarPartDamageAndImage(cl, trueItems) { cl.carPartDamage = trueItems; const jsonTrueItems = JSON.parse(JSON.stringify(trueItems)); @@ -389,7 +551,9 @@ export class ClaimRequestManagementService { /** * Field expert completes claim-needed data for expert-initiated IN_PERSON files. - * Only the initiating expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts. + * Only the initiating field expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts. + * + * V2: Resolves `ClaimCase` in `claimCases` collection (workflow). Legacy v1 claim documents are still supported. */ async expertCompleteClaimData( claimRequestId: string, @@ -401,6 +565,16 @@ export class ClaimRequestManagementService { otherParts?: any[]; }, ): Promise<{ success: boolean; claimRequestId: string }> { + const claimCase = await this.claimCaseDbService.findById(claimRequestId); + if (claimCase) { + return this.expertCompleteClaimDataForClaimCaseV2( + claimRequestId, + expert, + dto, + claimCase, + ); + } + const claim = await this.claimDbService.findOne(claimRequestId); if (!claim) { throw new NotFoundException("Claim file not found"); @@ -422,6 +596,7 @@ export class ClaimRequestManagementService { if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) { throw new BadRequestException("Car part damage is already set."); } + this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage); const trueItems = this.findTrueItems(dto.carPartDamage); const claimDoc = (await this.claimDbService.findOne( claimRequestId, @@ -450,6 +625,178 @@ export class ClaimRequestManagementService { return { success: true, claimRequestId }; } + /** + * V2 ClaimCase: outer parts + optional bank/other parts (expert-initiated IN_PERSON only). + */ + private async expertCompleteClaimDataForClaimCaseV2( + claimRequestId: string, + expert: any, + dto: { + carPartDamage?: CarDamagePartDto; + sheba?: string; + nationalCodeOfInsurer?: string; + otherParts?: any[]; + }, + claimCase: any, + ): Promise<{ success: boolean; claimRequestId: string }> { + await this.assertFieldExpertInPersonBlameForClaimCase(claimCase, expert); + + let working = claimCase; + + if (dto.carPartDamage) { + if (working.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OUTER_PARTS) { + throw new BadRequestException( + `Cannot set outer parts: workflow step must be ${ClaimWorkflowStep.SELECT_OUTER_PARTS}. Current: ${working.workflow?.currentStep}`, + ); + } + if ( + working.damage?.selectedParts && + working.damage.selectedParts.length > 0 + ) { + throw new BadRequestException("Car part damage is already set."); + } + this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage); + const selectedParts = this.carDamagePartDtoToOuterPartSlugs(dto.carPartDamage); + if (!selectedParts.length) { + throw new BadRequestException( + "No outer damaged parts could be derived from carPartDamage. Check part selections.", + ); + } + + const updated = await this.claimCaseDbService.findByIdAndUpdate( + claimRequestId, + { + "damage.selectedParts": selectedParts, + status: ClaimCaseStatus.SELECTING_OTHER_PARTS, + "workflow.currentStep": ClaimWorkflowStep.SELECT_OTHER_PARTS, + "workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + $push: { + "workflow.completedSteps": ClaimWorkflowStep.SELECT_OUTER_PARTS, + history: { + type: "STEP_COMPLETED", + actor: { + actorId: new Types.ObjectId(expert.sub), + actorName: + `${expert.firstName || ""} ${expert.lastName || ""}`.trim() || + "field_expert", + actorType: "field_expert", + }, + timestamp: new Date(), + metadata: { + stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS, + selectedParts, + description: "Field expert submitted outer damage parts", + }, + }, + }, + }, + ); + if (!updated) { + throw new InternalServerErrorException("Failed to update claim case"); + } + working = updated; + } + + const hasBankOrOther = + dto.sheba != null || + dto.nationalCodeOfInsurer != null || + dto.otherParts != null; + if (hasBankOrOther) { + const fresh = await this.claimCaseDbService.findById(claimRequestId); + if (!fresh) throw new NotFoundException("Claim case not found"); + if (fresh.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OTHER_PARTS) { + throw new BadRequestException( + `Bank/other parts can only be set at ${ClaimWorkflowStep.SELECT_OTHER_PARTS}. Current: ${fresh.workflow?.currentStep}`, + ); + } + + const mergedSheba = + dto.sheba != null ? dto.sheba : fresh.money?.sheba; + const mergedNational = + dto.nationalCodeOfInsurer != null + ? dto.nationalCodeOfInsurer + : fresh.money?.nationalCodeOfInsurer; + + if (fresh.money?.sheba && dto.sheba != null && dto.sheba !== fresh.money.sheba) { + throw new ConflictException("Sheba is already set for this claim."); + } + if ( + fresh.money?.nationalCodeOfInsurer && + dto.nationalCodeOfInsurer != null && + dto.nationalCodeOfInsurer !== fresh.money.nationalCodeOfInsurer + ) { + throw new ConflictException( + "National code of insurer is already set for this claim.", + ); + } + + const otherPartsArr = this.normalizeExpertOtherPartsInput(dto.otherParts); + const setPatch: Record = {}; + if (dto.sheba != null) setPatch["money.sheba"] = dto.sheba; + if (dto.nationalCodeOfInsurer != null) { + setPatch["money.nationalCodeOfInsurer"] = dto.nationalCodeOfInsurer; + } + if (dto.otherParts != null) { + setPatch["damage.otherParts"] = + otherPartsArr.length > 0 ? otherPartsArr : []; + } + + const bankComplete = + mergedSheba && + mergedNational && + /^[0-9]{24}$/.test(String(mergedSheba).replace(/\s/g, "")) && + /^[0-9]{10}$/.test(String(mergedNational).replace(/\s/g, "")); + + if (bankComplete) { + const shebaNorm = String(mergedSheba).replace(/\s/g, ""); + const nationalNorm = String(mergedNational).replace(/\s/g, ""); + await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { + "money.sheba": shebaNorm, + "money.nationalCodeOfInsurer": nationalNorm, + ...(dto.otherParts != null + ? { + "damage.otherParts": + otherPartsArr.length > 0 ? otherPartsArr : [], + } + : {}), + status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS, + "workflow.currentStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + "workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + $push: { + "workflow.completedSteps": ClaimWorkflowStep.SELECT_OTHER_PARTS, + history: { + type: "STEP_COMPLETED", + actor: { + actorId: new Types.ObjectId(expert.sub), + actorName: + `${expert.firstName || ""} ${expert.lastName || ""}`.trim() || + "field_expert", + actorType: "field_expert", + }, + timestamp: new Date(), + metadata: { + stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS, + description: + "Field expert submitted bank info and optional other parts", + }, + }, + }, + }); + } else { + if (Object.keys(setPatch).length === 0) { + throw new BadRequestException( + "Provide sheba (24 digits) and nationalCodeOfInsurer (10 digits) together to finish this step, or supply partial fields incrementally.", + ); + } + await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { + $set: setPatch, + }); + } + } + + return { success: true, claimRequestId }; + } + async selectCarOtherPartDamage( requestId: string, body: any, // Use your DTO for better type safety From f8fbbb7ac6e9d89ccc03c672e49d3f8b1e0b9d53 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 15 Apr 2026 11:43:19 +0330 Subject: [PATCH 2/3] YARA-784 --- .../expert-complete-car-body-form.v2.dto.ts | 30 ++++++++++ .../dto/expert-complete-location.v2.dto.ts | 10 ++++ ...expert-complete-third-party-form.v2.dto.ts | 50 +++++++++++++++++ .../expert-initiated.v2.controller.ts | 40 ++++++------- .../request-management.service.ts | 56 +++++++++---------- 5 files changed, 131 insertions(+), 55 deletions(-) create mode 100644 src/request-management/dto/expert-complete-car-body-form.v2.dto.ts create mode 100644 src/request-management/dto/expert-complete-location.v2.dto.ts create mode 100644 src/request-management/dto/expert-complete-third-party-form.v2.dto.ts 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, From 640b240ada26a6d47b52e6240b88693fec6ce602 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 15 Apr 2026 11:57:09 +0330 Subject: [PATCH 3/3] YARA-791 --- .../request-management.service.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 17270fc..c948130 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -3872,10 +3872,19 @@ export class RequestManagementService { async getAllBlameRequestsV2(user: any): Promise { try { + const userIdFilter = + user?.sub && Types.ObjectId.isValid(user.sub) + ? { "parties.person.userId": new Types.ObjectId(user.sub) } + : null; + const phoneFilter = user?.username + ? { "parties.person.phoneNumber": user.username } + : null; + + const filters = [userIdFilter, phoneFilter].filter(Boolean); + if (filters.length === 0) return []; + const requests = await this.blameRequestDbService.find( - { - "parties.person.userId": new Types.ObjectId(user.sub), - }, + filters.length === 1 ? (filters[0] as any) : ({ $or: filters } as any), { select: "requestNo type status blameStatus createdAt updatedAt parties", } @@ -3883,7 +3892,9 @@ export class RequestManagementService { const enriched = requests.map((req: any) => { const party = req.parties.find( - (p: any) => String(p.person.userId) === String(user.sub) + (p: any) => + (p?.person?.userId && String(p.person.userId) === String(user.sub)) || + (p?.person?.phoneNumber && p.person.phoneNumber === user?.username), ); const obj = req.toObject();