From 7f5b64f2a6621c6f1822a13108812003489b8c60 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 18 Apr 2026 14:02:50 +0330 Subject: [PATCH 1/2] Fixed DTO --- src/expert-blame/expert-blame.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index 60c0f42..a033e59 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -599,7 +599,7 @@ export class ExpertBlameService { // Exclude mapped inquiry vehicle for both parties from response for (const party of parties) { - delete party.vehicle; + delete (party.vehicle as any).inquiry; } return doc; From 0086b8db4d67740c11f9d03afe21e63aa1cf3efd Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 18 Apr 2026 17:04:19 +0330 Subject: [PATCH 2/2] fixed many bugs and step issues --- .../claim-workflow-steps.enum.ts | 10 +- .../claim-request-management.service.ts | 91 +++++--- .../claim-request-management.v2.controller.ts | 21 +- .../dto/select-other-parts-v2.dto.ts | 7 +- .../schema/claim-case.identity.schema.ts | 3 + src/expert-claim/dto/claim-detail-v2.dto.ts | 14 ++ src/expert-claim/dto/claim-list-v2.dto.ts | 15 ++ src/expert-claim/expert-claim.service.ts | 218 ++++++++++++++++-- src/helpers/tenant-scope.ts | 7 +- .../dto/claim-creation-hint.dto.ts | 19 ++ .../request-management.service.ts | 85 ++++++- .../request-management.v2.controller.ts | 6 + .../dto/claim-workflow-step.examples.ts | 16 +- .../dto/workflow-step.examples.ts | 20 +- 14 files changed, 443 insertions(+), 89 deletions(-) create mode 100644 src/request-management/dto/claim-creation-hint.dto.ts diff --git a/src/Types&Enums/claim-request-management/claim-workflow-steps.enum.ts b/src/Types&Enums/claim-request-management/claim-workflow-steps.enum.ts index 2129b2b..df705d1 100644 --- a/src/Types&Enums/claim-request-management/claim-workflow-steps.enum.ts +++ b/src/Types&Enums/claim-request-management/claim-workflow-steps.enum.ts @@ -6,12 +6,12 @@ export enum ClaimWorkflowStep { // User: Damage selection phase SELECT_OUTER_PARTS = "SELECT_OUTER_PARTS", SELECT_OTHER_PARTS = "SELECT_OTHER_PARTS", - - // User: Documentation phase - UPLOAD_REQUIRED_DOCUMENTS = "UPLOAD_REQUIRED_DOCUMENTS", - - // User: Damage capture phase (per part) + + // User: Damage capture phase (angles + per-part photos) — before document upload in v2 CAPTURE_PART_DAMAGES = "CAPTURE_PART_DAMAGES", + + // User: Documentation phase — after captures in v2 + UPLOAD_REQUIRED_DOCUMENTS = "UPLOAD_REQUIRED_DOCUMENTS", // User submission complete USER_SUBMISSION_COMPLETE = "USER_SUBMISSION_COMPLETE", diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 23e5399..8f8abf3 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -669,7 +669,7 @@ export class ClaimRequestManagementService { "damage.selectedParts": selectedParts, status: ClaimCaseStatus.SELECTING_OTHER_PARTS, "workflow.currentStep": ClaimWorkflowStep.SELECT_OTHER_PARTS, - "workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + "workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES, $push: { "workflow.completedSteps": ClaimWorkflowStep.SELECT_OUTER_PARTS, history: { @@ -759,9 +759,9 @@ export class ClaimRequestManagementService { otherPartsArr.length > 0 ? otherPartsArr : [], } : {}), - status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS, - "workflow.currentStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, - "workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + status: ClaimCaseStatus.CAPTURING_PART_DAMAGES, + "workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + "workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, $push: { "workflow.completedSteps": ClaimWorkflowStep.SELECT_OTHER_PARTS, history: { @@ -3184,6 +3184,13 @@ export class ClaimRequestManagementService { owner: { userId: new Types.ObjectId(currentUserId), userRole: currentUserParty.role as any, + ...(currentUserParty.person?.clientId + ? { + clientId: new Types.ObjectId( + currentUserParty.person.clientId as any, + ), + } + : {}), }, history: [ { @@ -3305,6 +3312,11 @@ export class ClaimRequestManagementService { owner: { userId: new Types.ObjectId(damagedUserId), userRole: damagedParty?.role as any, + ...(damagedParty?.person?.clientId + ? { + clientId: new Types.ObjectId(damagedParty.person.clientId as any), + } + : {}), }, history: [ { @@ -3440,6 +3452,11 @@ export class ClaimRequestManagementService { owner: { userId: new Types.ObjectId(damagedUserId), userRole: damagedParty?.role as any, + ...(damagedParty?.person?.clientId + ? { + clientId: new Types.ObjectId(damagedParty.person.clientId as any), + } + : {}), }, createdByRegistrarId: new Types.ObjectId(registrar.sub), history: [ @@ -3550,7 +3567,7 @@ export class ClaimRequestManagementService { 'damage.selectedParts': selectedParts, 'status': ClaimCaseStatus.SELECTING_OTHER_PARTS, 'workflow.currentStep': ClaimWorkflowStep.SELECT_OTHER_PARTS, - 'workflow.nextStep': ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + 'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES, $push: { 'workflow.completedSteps': ClaimWorkflowStep.SELECT_OUTER_PARTS, history: { @@ -3586,7 +3603,7 @@ export class ClaimRequestManagementService { publicId: updatedClaim.publicId, selectedParts: selectedParts, currentStep: ClaimWorkflowStep.SELECT_OTHER_PARTS, - nextStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, message: 'Outer parts selected successfully. Please proceed to select other parts and provide bank information.', }; } catch (error) { @@ -3704,9 +3721,9 @@ export class ClaimRequestManagementService { 'damage.otherParts': otherParts.length > 0 ? otherParts : undefined, 'money.sheba': shebaNumber, 'money.nationalCodeOfInsurer': nationalCode, - 'status': ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS, + 'status': ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS , 'workflow.currentStep': ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, - 'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + 'workflow.nextStep': ClaimWorkflowStep.USER_SUBMISSION_COMPLETE, $push: { 'workflow.completedSteps': ClaimWorkflowStep.SELECT_OTHER_PARTS, history: { @@ -3785,9 +3802,10 @@ export class ClaimRequestManagementService { otherParts: otherParts, shebaNumber: maskedSheba, nationalCodeOfOwner: maskedNationalCode, - currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, - nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, - message: 'Other parts and bank information saved successfully. Please proceed to upload required documents.', + currentStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + nextStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + message: + 'Other parts and bank information saved successfully. Please capture car angles and damaged parts, then upload required documents.', }; } catch (error) { if (error instanceof HttpException) throw error; @@ -3854,7 +3872,6 @@ export class ClaimRequestManagementService { : null; const isCarBody = blameRequest?.type === BlameRequestType.CAR_BODY; const requiredDocsDefinition = [ - { key: 'car_green_card', label_fa: 'کارت سبز خودرو', label_en: 'Car Green Card', category: 'general' }, { key: 'damaged_driving_license_front', label_fa: 'گواهینامه طرف آسیب‌دیده - جلو', label_en: 'Damaged Party License - Front', category: 'damaged_party' }, { key: 'damaged_driving_license_back', label_fa: 'گواهینامه طرف آسیب‌دیده - پشت', label_en: 'Damaged Party License - Back', category: 'damaged_party' }, { key: 'damaged_chassis_number', label_fa: 'شماره شاسی', label_en: 'Chassis Number', category: 'damaged_party' }, @@ -4036,11 +4053,14 @@ export class ClaimRequestManagementService { const uploadedCount = (currentDocs instanceof Map ? currentDocs.size : Object.keys(currentDocs).length) + 1; const allDocumentsUploaded = uploadedCount >= totalDocsRequired; - // If all documents uploaded, move to next step + // If all documents uploaded, user flow complete (captures were done earlier in v2) if (allDocumentsUploaded) { - updateData['status'] = ClaimCaseStatus.CAPTURING_PART_DAMAGES; - updateData['workflow.currentStep'] = ClaimWorkflowStep.CAPTURE_PART_DAMAGES; - updateData['workflow.nextStep'] = ClaimWorkflowStep.USER_SUBMISSION_COMPLETE; + updateData['status'] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT; + updateData['claimStatus'] = ClaimStatus.PENDING; + updateData['workflow.currentStep'] = + ClaimWorkflowStep.USER_SUBMISSION_COMPLETE; + updateData['workflow.nextStep'] = + ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT; updateData.$push.history = [ updateData.$push.history, { @@ -4056,15 +4076,30 @@ export class ClaimRequestManagementService { description: 'All required documents uploaded', }, }, + { + type: 'STEP_COMPLETED', + actor: { + actorId: new Types.ObjectId(currentUserId), + actorName: claimCase.owner?.fullName || 'User', + actorType: 'user', + }, + timestamp: new Date(), + metadata: { + stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE, + description: + 'User submission complete. Claim ready for damage expert review.', + }, + }, ]; - updateData.$push['workflow.completedSteps'] = ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS; + updateData.$push['workflow.completedSteps'] = + ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS; } await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updateData); const remaining = totalDocsRequired - uploadedCount; const message = allDocumentsUploaded - ? 'All documents uploaded successfully. Please proceed to capture car angles and damaged parts.' + ? 'All documents uploaded successfully. Your claim is now ready for damage expert review.' : `Document uploaded successfully. ${remaining} documents remaining.`; return { @@ -4072,7 +4107,9 @@ export class ClaimRequestManagementService { documentKey: body.documentKey, fileUrl, allDocumentsUploaded, - currentStep: allDocumentsUploaded ? ClaimWorkflowStep.CAPTURE_PART_DAMAGES : ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + currentStep: allDocumentsUploaded + ? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE + : ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, message, }; } catch (error) { @@ -4169,10 +4206,9 @@ export class ClaimRequestManagementService { if (allCapturesComplete) { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { - status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT, - claimStatus: ClaimStatus.PENDING, - 'workflow.currentStep': ClaimWorkflowStep.USER_SUBMISSION_COMPLETE, - 'workflow.nextStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT, + status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS, + 'workflow.currentStep': ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + 'workflow.nextStep': ClaimWorkflowStep.USER_SUBMISSION_COMPLETE, $push: { 'workflow.completedSteps': ClaimWorkflowStep.CAPTURE_PART_DAMAGES, history: { @@ -4184,8 +4220,9 @@ export class ClaimRequestManagementService { }, timestamp: new Date(), metadata: { - stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE, - description: 'User submission complete. Claim ready for damage expert review.', + stepKey: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + description: + 'All car angles and damaged parts captured. Please upload required documents.', }, }, }, @@ -4194,7 +4231,7 @@ export class ClaimRequestManagementService { const remaining = (4 - anglesCaptured) + (selectedParts.length - partsCaptured); const message = allCapturesComplete - ? 'All captures complete. Your claim is now ready for damage expert review.' + ? 'All captures complete. Please proceed to upload required documents.' : `${body.captureType === 'angle' ? 'Angle' : 'Part'} captured successfully. ${remaining} captures remaining.`; return { @@ -4204,7 +4241,7 @@ export class ClaimRequestManagementService { fileUrl, allCapturesComplete, currentStep: allCapturesComplete - ? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE + ? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS : ClaimWorkflowStep.CAPTURE_PART_DAMAGES, message, }; diff --git a/src/claim-request-management/claim-request-management.v2.controller.ts b/src/claim-request-management/claim-request-management.v2.controller.ts index d91deac..767cdc9 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -270,8 +270,8 @@ Optional: upload car green card file in the same step. - headlight, taillight, mirror, glass **After Success:** -- Workflow moves to: UPLOAD_REQUIRED_DOCUMENTS (Step 4) -- User can proceed to upload required documents +- Workflow moves to: CAPTURE_PART_DAMAGES (Step 4) +- User captures car angles and damaged-part photos, then uploads required documents (Step 5) `, }) @ApiParam({ @@ -371,6 +371,8 @@ Optional: upload car green card file in the same step. - Damaged parts (based on selected outer parts) Returns status of each item (uploaded/captured or not). + +**V2 order:** Complete angles and part photos first, then required documents. `, }) @ApiParam({ @@ -433,9 +435,9 @@ Returns status of each item (uploaded/captured or not). ) @ApiConsumes("multipart/form-data") @ApiOperation({ - summary: "Upload Required Document (V2 - Step 4)", + summary: "Upload Required Document (V2 - Step 5)", description: ` -**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 4 of Claim) +**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim) **Upload one of the required documents** (13 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements): - car_green_card @@ -444,7 +446,7 @@ Returns status of each item (uploaded/captured or not). - damaged_car_card_front/back, damaged_metal_plate - guilty_driving_license_front/back, guilty_car_card_front/back, guilty_metal_plate (THIRD_PARTY) -**When all required documents are uploaded:** Workflow moves to CAPTURE_PART_DAMAGES (Step 5). +**When all required documents are uploaded:** Workflow moves to USER_SUBMISSION_COMPLETE and the claim is ready for damage expert review. **Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to upload documents on behalf of the damaged party. `, @@ -462,7 +464,6 @@ Returns status of each item (uploaded/captured or not). documentKey: { type: "string", enum: [ - "car_green_card", "damaged_driving_license_front", "damaged_driving_license_back", "damaged_chassis_number", @@ -476,7 +477,7 @@ Returns status of each item (uploaded/captured or not). "guilty_car_card_back", "guilty_metal_plate", ], - example: "car_green_card", + example: "damaged_driving_license_front", }, file: { type: "string", @@ -542,15 +543,15 @@ Returns status of each item (uploaded/captured or not). ) @ApiConsumes("multipart/form-data") @ApiOperation({ - summary: "Capture Car Angle or Damaged Part (V2 - Step 5)", + summary: "Capture Car Angle or Damaged Part (V2 - Step 4)", description: ` -**Workflow Step:** CAPTURE_PART_DAMAGES (Step 5 of Claim) +**Workflow Step:** CAPTURE_PART_DAMAGES (Step 4 of Claim) **Capture types:** 1. **angle**: Car angles (front, back, left, right) - 4 required 2. **part**: Damaged parts based on selectedParts from Step 2 -**All captures must be completed before user can submit claim.** +**When all captures are complete:** Workflow moves to UPLOAD_REQUIRED_DOCUMENTS (Step 5). **Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to capture photos on behalf of the damaged party. `, diff --git a/src/claim-request-management/dto/select-other-parts-v2.dto.ts b/src/claim-request-management/dto/select-other-parts-v2.dto.ts index 8b3fe3e..9e2f69b 100644 --- a/src/claim-request-management/dto/select-other-parts-v2.dto.ts +++ b/src/claim-request-management/dto/select-other-parts-v2.dto.ts @@ -117,19 +117,20 @@ export class SelectOtherPartsV2ResponseDto { @ApiProperty({ description: 'Current workflow step', - example: 'UPLOAD_REQUIRED_DOCUMENTS', + example: 'CAPTURE_PART_DAMAGES', }) currentStep: string; @ApiProperty({ description: 'Next possible workflow step', - example: 'CAPTURE_PART_DAMAGES', + example: 'UPLOAD_REQUIRED_DOCUMENTS', }) nextStep: string; @ApiProperty({ description: 'Success message', - example: 'Other parts and bank information saved successfully. Please proceed to upload required documents.', + example: + 'Other parts and bank information saved successfully. Please capture car angles and damaged parts, then upload required documents.', }) message: string; } diff --git a/src/claim-request-management/entites/schema/claim-case.identity.schema.ts b/src/claim-request-management/entites/schema/claim-case.identity.schema.ts index 31b11f9..d01912b 100644 --- a/src/claim-request-management/entites/schema/claim-case.identity.schema.ts +++ b/src/claim-request-management/entites/schema/claim-case.identity.schema.ts @@ -50,6 +50,9 @@ export class ClaimOwner { @Prop({ type: String }) fullName?: string; + + @Prop({ type: Types.ObjectId }) + clientId: Types.ObjectId; } export const ClaimOwnerSchema = SchemaFactory.createForClass(ClaimOwner); diff --git a/src/expert-claim/dto/claim-detail-v2.dto.ts b/src/expert-claim/dto/claim-detail-v2.dto.ts index 84f205b..d07ffc9 100644 --- a/src/expert-claim/dto/claim-detail-v2.dto.ts +++ b/src/expert-claim/dto/claim-detail-v2.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BlameRequestType } from 'src/Types&Enums/blame-request-management/blameRequestType.enum'; export class ClaimDetailV2ResponseDto { @ApiProperty() @@ -43,6 +44,19 @@ export class ClaimDetailV2ResponseDto { plate?: any; }; + @ApiPropertyOptional({ + description: 'Blame file type from linked blame case', + enum: BlameRequestType, + }) + blameRequestType?: BlameRequestType; + + @ApiPropertyOptional({ + description: + 'CAR_BODY only: first-step flags — another car (`car`) and/or object (`object`)', + example: { car: true, object: false }, + }) + carBodyFirstForm?: { car?: boolean; object?: boolean }; + @ApiPropertyOptional({ description: 'Blame request ID', example: '507f...' }) blameRequestId?: string; diff --git a/src/expert-claim/dto/claim-list-v2.dto.ts b/src/expert-claim/dto/claim-list-v2.dto.ts index 7141e27..0c658bb 100644 --- a/src/expert-claim/dto/claim-list-v2.dto.ts +++ b/src/expert-claim/dto/claim-list-v2.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BlameRequestType } from 'src/Types&Enums/blame-request-management/blameRequestType.enum'; export class ClaimListItemV2Dto { @ApiProperty({ description: 'Claim case ID' }) @@ -29,6 +30,20 @@ export class ClaimListItemV2Dto { carType?: string; }; + @ApiPropertyOptional({ + description: 'Blame file type from linked blame case', + enum: BlameRequestType, + example: BlameRequestType.THIRD_PARTY, + }) + blameRequestType?: BlameRequestType; + + @ApiPropertyOptional({ + description: + 'CAR_BODY only: first-step flags — accident involved another car (`car`) and/or fixed object (`object`)', + example: { car: true, object: false }, + }) + carBodyFirstForm?: { car?: boolean; object?: boolean }; + @ApiProperty({ description: 'Submission date', example: '2026-02-22T10:00:00.000Z' }) createdAt: string; } diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index c3d0332..0977075 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -38,6 +38,8 @@ import { requireActorClientKey, } from "src/helpers/tenant-scope"; import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum"; +import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum"; +import { PartyRole } from "src/request-management/entities/schema/partyRole.enum"; import { GetClaimListV2ResponseDto, ClaimListItemV2Dto } from "./dto/claim-list-v2.dto"; import { ClaimDetailV2ResponseDto } from "./dto/claim-detail-v2.dto"; import { ClaimSubmitReplyDto, ClaimSubmitResendDto } from "./dto/reply.dto"; @@ -47,6 +49,7 @@ import { ClaimRequiredDocumentDbService } from "src/claim-request-management/ent import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum"; import { DaghiOption } from "src/Types&Enums/claim-request-management/daghi-option.enum"; import { BranchDbService } from "src/client/entities/db-service/branch.db.service"; +import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; @Injectable() export class ExpertClaimService { @@ -165,8 +168,120 @@ export class ExpertClaimService { private readonly claimFactorsImageDbService: ClaimFactorsImageDbService, private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService, private readonly branchDbService: BranchDbService, + private readonly blameRequestDbService: BlameRequestDbService, ) {} + /** Map blame party `Vehicle` (name/model/type) to expert panel shape. */ + private expertVehicleFromPartyVehicle(v: any): { + carName?: string; + carModel?: string; + carType?: string; + plate?: { plateId?: string }; + } | undefined { + if (!v || typeof v !== "object") return undefined; + const carName = v.name ?? v.carName; + const carModel = v.model ?? v.carModel; + const carType = v.type ?? v.carType; + const plateId = v.plateId; + const plate = + plateId != null && String(plateId).length > 0 + ? { plateId: String(plateId) } + : undefined; + if (!carName && !carModel && !carType && !plate) return undefined; + return { carName, carModel, carType, ...(plate ? { plate } : {}) }; + } + + /** Damaged party vehicle on blame (matches claim owner, or non-guilty party for THIRD_PARTY). */ + private damagedPartyVehicleFromBlame( + blame: any, + claim: any, + ): + | { + carName?: string; + carModel?: string; + carType?: string; + plate?: { plateId?: string }; + } + | undefined { + const parties = blame?.parties; + if (!Array.isArray(parties) || parties.length === 0) return undefined; + + const ownerId = claim?.owner?.userId ? String(claim.owner.userId) : ""; + let party = ownerId + ? parties.find( + (p: any) => + p?.person?.userId && String(p.person.userId) === ownerId, + ) + : undefined; + + if (!party && blame?.expert?.decision?.guiltyPartyId) { + const guilty = String(blame.expert.decision.guiltyPartyId); + party = parties.find( + (p: any) => + p?.person?.userId && String(p.person.userId) !== guilty, + ); + } + + if (!party && parties.length === 1) party = parties[0]; + + return this.expertVehicleFromPartyVehicle(party?.vehicle); + } + + private vehicleForExpertFromClaimAndBlameMap( + claim: any, + blameById: Map, + ): + | { carName?: string; carModel?: string; carType?: string; plate?: { plateId?: string } } + | undefined { + const cv = claim?.vehicle; + if ( + cv && + (cv.carName || cv.carModel || cv.carType || (cv as any).plate) + ) { + return { + carName: cv.carName, + carModel: cv.carModel, + carType: cv.carType, + ...((cv as any).plate ? { plate: (cv as any).plate } : {}), + }; + } + const bid = claim?.blameRequestId?.toString(); + if (!bid) return undefined; + const blame = blameById.get(bid); + if (!blame) return undefined; + return this.damagedPartyVehicleFromBlame(blame, claim); + } + + /** + * Blame file kind for damage-expert UI: THIRD_PARTY vs CAR_BODY, and CAR_BODY first-step flags. + */ + private blameFileContextForExpert(blame: any): { + blameRequestType?: BlameRequestType; + carBodyFirstForm?: { car?: boolean; object?: boolean }; + } { + if (!blame?.type) return {}; + const blameRequestType = blame.type as BlameRequestType; + const out: { + blameRequestType?: BlameRequestType; + carBodyFirstForm?: { car?: boolean; object?: boolean }; + } = { blameRequestType }; + if (blameRequestType !== BlameRequestType.CAR_BODY) return out; + + const parties = blame.parties; + if (!Array.isArray(parties) || parties.length === 0) return out; + + const first = + parties.find((p: any) => p?.role === PartyRole.FIRST) ?? parties[0]; + const cbf = first?.carBodyFirstForm; + if (cbf && typeof cbf === "object") { + out.carBodyFirstForm = { + ...(typeof cbf.car === "boolean" ? { car: cbf.car } : {}), + ...(typeof cbf.object === "boolean" ? { object: cbf.object } : {}), + }; + } + return out; + } + private isVisibleToClientType(client: any, actor: any): boolean { if (actor.userType === UserType.GENUINE) { return true; @@ -1858,29 +1973,57 @@ export class ExpertClaimService { ], }); - const list = (claims as any[]) - .filter((c) => claimCaseTouchesClient(c, clientKey)) - .map((c) => ({ - claimRequestId: c._id.toString(), - publicId: c.publicId, - status: c.status, - currentStep: c.workflow?.currentStep || '', - locked: c.workflow?.locked || false, - lockedBy: c.workflow?.lockedBy - ? { - actorId: c.workflow.lockedBy.actorId?.toString(), - actorName: c.workflow.lockedBy.actorName, - } - : undefined, - vehicle: c.vehicle - ? { - carName: c.vehicle.carName, - carModel: c.vehicle.carModel, - carType: c.vehicle.carType, - } - : undefined, - createdAt: c.createdAt, - })) as ClaimListItemV2Dto[]; + const filtered = (claims as any[]).filter((c) => + claimCaseTouchesClient(c, clientKey), + ); + + const blameIds = [ + ...new Set( + filtered + .map((c) => c.blameRequestId?.toString()) + .filter((id): id is string => !!id), + ), + ]; + const blames = + blameIds.length > 0 + ? ((await this.blameRequestDbService.find( + { _id: { $in: blameIds.map((id) => new Types.ObjectId(id)) } }, + { lean: true, select: "type parties expert.decision" }, + )) as any[]) + : []; + const blameById = new Map( + blames.map((b) => [String(b._id), b]), + ); + + const list = filtered.map((c) => { + const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById); + const blame = c.blameRequestId + ? blameById.get(c.blameRequestId.toString()) + : undefined; + const fileCtx = blame ? this.blameFileContextForExpert(blame) : {}; + return { + claimRequestId: c._id.toString(), + publicId: c.publicId, + status: c.status, + currentStep: c.workflow?.currentStep || "", + locked: c.workflow?.locked || false, + lockedBy: c.workflow?.lockedBy + ? { + actorId: c.workflow.lockedBy.actorId?.toString(), + actorName: c.workflow.lockedBy.actorName, + } + : undefined, + vehicle: v + ? { + carName: v.carName, + carModel: v.carModel, + carType: v.carType, + } + : undefined, + ...fileCtx, + createdAt: c.createdAt, + }; + }) as ClaimListItemV2Dto[]; return { list, total: list.length }; } @@ -1967,6 +2110,32 @@ export class ExpertClaimService { }; } + let vehiclePayload = claim.vehicle as any; + const hasClaimVehicle = + vehiclePayload && + (vehiclePayload.carName || + vehiclePayload.carModel || + vehiclePayload.carType || + vehiclePayload.plate); + + let blameLean: any = null; + if (claim.blameRequestId) { + const rows = (await this.blameRequestDbService.find( + { _id: new Types.ObjectId(claim.blameRequestId.toString()) }, + { lean: true, select: "type parties expert.decision" }, + )) as any[]; + blameLean = rows[0] ?? null; + } + + if (!hasClaimVehicle && blameLean) { + const fromBlame = this.damagedPartyVehicleFromBlame(blameLean, claim); + if (fromBlame) vehiclePayload = fromBlame; + } + + const blameFileContext = blameLean + ? this.blameFileContextForExpert(blameLean) + : {}; + return { claimRequestId: claim._id.toString(), publicId: claim.publicId, @@ -1988,7 +2157,8 @@ export class ExpertClaimService { fullName: claim.owner.fullName, } : undefined, - vehicle: claim.vehicle, + vehicle: vehiclePayload, + ...blameFileContext, blameRequestId: claim.blameRequestId?.toString(), blameRequestNo: claim.blameRequestNo, selectedParts: claim.damage?.selectedParts, diff --git a/src/helpers/tenant-scope.ts b/src/helpers/tenant-scope.ts index b87bc91..8f5eb9b 100644 --- a/src/helpers/tenant-scope.ts +++ b/src/helpers/tenant-scope.ts @@ -19,7 +19,12 @@ export function blameCaseTouchesClient(doc: any, clientKey: string): boolean { } export function claimCaseTouchesClient(doc: any, clientKey: string): boolean { - return String(doc?.owner?.userClientKey ?? "") === String(clientKey); + const id = String(clientKey); + const owner = doc?.owner as + | { clientId?: unknown; userClientKey?: unknown } + | undefined; + const tenantId = owner?.clientId ?? owner?.userClientKey; + return String(tenantId ?? "") === id; } /** diff --git a/src/request-management/dto/claim-creation-hint.dto.ts b/src/request-management/dto/claim-creation-hint.dto.ts new file mode 100644 index 0000000..1b7ded5 --- /dev/null +++ b/src/request-management/dto/claim-creation-hint.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; + +/** + * Computed on GET blame v2 for the current viewer — no DB field on BlameRequest. + */ +export class ClaimCreationHintDto { + @ApiProperty({ + description: + "True if a v2 claim case already exists for this blame (by blameRequestId).", + }) + hasClaim: boolean; + + @ApiProperty({ + description: + "True when this viewer is the damaged party, blame is COMPLETED, no claim exists yet, " + + "and for THIRD_PARTY all parties have signed and accepted the expert decision.", + }) + shouldGuideToCreateClaim: boolean; +} diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index c948130..90dbe06 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -46,6 +46,7 @@ import { RequestManagementModel } from "./entities/schema/request-management.sch import { BlameDocumentType } from "./entities/schema/blame-document.schema"; import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service"; import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service"; +import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service"; import { CreationMethod, FilledBy, @@ -300,6 +301,7 @@ export class RequestManagementService { private readonly expertDbService: ExpertDbService, private readonly autoCloseRequestService: AutoCloseRequestService, private readonly claimRequestManagementDbService: ClaimRequestManagementDbService, + private readonly claimCaseDbService: ClaimCaseDbService, private readonly publicIdService: PublicIdService, private readonly workflowStepDbService: WorkflowStepDbService, private readonly hashService: HashService, @@ -3916,6 +3918,81 @@ export class RequestManagementService { + /** + * V2: Hint for UX — damaged user should open create-claim when blame is done and no claim exists. + * Mirrors eligibility rules in createClaimFromBlameV2 (without throwing). + */ + private async computeClaimCreationHintForViewer( + blame: any, + user: any, + ): Promise<{ hasClaim: boolean; shouldGuideToCreateClaim: boolean }> { + const none = { hasClaim: false, shouldGuideToCreateClaim: false }; + if (!user?.sub || !Types.ObjectId.isValid(String(user.sub))) { + return none; + } + const currentUserId = String(user.sub); + + const existingClaim = await this.claimCaseDbService.findOne({ + blameRequestId: blame._id, + }); + const hasClaim = !!existingClaim; + if (hasClaim) { + return { hasClaim: true, shouldGuideToCreateClaim: false }; + } + + if (blame.status !== CaseStatus.COMPLETED) { + return { hasClaim: false, shouldGuideToCreateClaim: false }; + } + + const parties = blame.parties || []; + + if (blame.type === BlameRequestType.CAR_BODY) { + const first = parties.find( + (p: any) => p?.role === PartyRole.FIRST || p?.role === "FIRST", + ); + const damagedId = first?.person?.userId + ? String(first.person.userId) + : null; + if (!damagedId || damagedId !== currentUserId) { + return { hasClaim: false, shouldGuideToCreateClaim: false }; + } + return { hasClaim: false, shouldGuideToCreateClaim: true }; + } + + if (blame.type === BlameRequestType.THIRD_PARTY) { + const guiltyRaw = blame.expert?.decision?.guiltyPartyId; + if (!guiltyRaw) { + return { hasClaim: false, shouldGuideToCreateClaim: false }; + } + const guiltyId = String(guiltyRaw); + if (currentUserId === guiltyId) { + return { hasClaim: false, shouldGuideToCreateClaim: false }; + } + const userParty = parties.find( + (p: any) => + p?.person?.userId && String(p.person.userId) === currentUserId, + ); + if (!userParty) { + return { hasClaim: false, shouldGuideToCreateClaim: false }; + } + if (parties.length < 2) { + return { hasClaim: false, shouldGuideToCreateClaim: false }; + } + const allSigned = parties.every( + (p: any) => p.confirmation !== undefined && p.confirmation !== null, + ); + const allAccepted = parties.every( + (p: any) => p.confirmation?.accepted === true, + ); + if (!allSigned || !allAccepted) { + return { hasClaim: false, shouldGuideToCreateClaim: false }; + } + return { hasClaim: false, shouldGuideToCreateClaim: true }; + } + + return none; + } + /** * 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. @@ -3958,7 +4035,13 @@ export class RequestManagementService { await (req as any).save(); } } - return req; + + const claimCreation = await this.computeClaimCreationHintForViewer(req, user); + const plain = + typeof (req as any).toObject === "function" + ? (req as any).toObject({ versionKey: false }) + : { ...(req as any) }; + return { ...plain, claimCreation }; } /** diff --git a/src/request-management/request-management.v2.controller.ts b/src/request-management/request-management.v2.controller.ts index 12f4831..56d1266 100644 --- a/src/request-management/request-management.v2.controller.ts +++ b/src/request-management/request-management.v2.controller.ts @@ -16,6 +16,7 @@ import { ApiBearerAuth, ApiBody, ApiConsumes, + ApiOperation, ApiParam, ApiTags, } from "@nestjs/swagger"; @@ -77,6 +78,11 @@ export class RequestManagementV2Controller { * Get one blame request by id. Allowed for the request owner (party) or the initiating field expert. */ @Get(":requestId") + @ApiOperation({ + summary: "Get one blame request (v2)", + description: + "Response is the blame document plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }) — computed for the current user so the app can route the damaged party to create-claim when the blame is COMPLETED and no v2 claim exists yet.", + }) @ApiParam({ name: "requestId", description: "Blame request ID" }) @Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT) async getBlameRequestV2( diff --git a/src/workflow-step-management/dto/claim-workflow-step.examples.ts b/src/workflow-step-management/dto/claim-workflow-step.examples.ts index 6231620..4ba76be 100644 --- a/src/workflow-step-management/dto/claim-workflow-step.examples.ts +++ b/src/workflow-step-management/dto/claim-workflow-step.examples.ts @@ -102,7 +102,7 @@ export const createClaimWorkflowStepExamples = { description_en: 'Select other damaged parts (non-body), Sheba number and insurer national code', category: 'CLAIM', requiredPreviousSteps: ['SELECT_OUTER_PARTS'], - nextPossibleSteps: ['UPLOAD_REQUIRED_DOCUMENTS'], + nextPossibleSteps: ['CAPTURE_PART_DAMAGES'], allowedRoles: ['user'], estimatedDuration: 4, fields: [ @@ -188,15 +188,15 @@ export const createClaimWorkflowStepExamples = { value: { stepKey: 'UPLOAD_REQUIRED_DOCUMENTS', type: 'CLAIM', - stepNumber: 4, + stepNumber: 5, isActive: true, stepName_fa: 'آپلود مدارک مورد نیاز', stepName_en: 'Upload Required Documents', description_fa: 'آپلود مدارک مورد نیاز برای بررسی خسارت (لیست داینامیک بر اساس قطعات انتخاب شده)', description_en: 'Upload required documents for claim review (dynamic list based on selected parts)', category: 'CLAIM', - requiredPreviousSteps: ['SELECT_OTHER_PARTS'], - nextPossibleSteps: ['CAPTURE_PART_DAMAGES'], + requiredPreviousSteps: ['CAPTURE_PART_DAMAGES'], + nextPossibleSteps: ['USER_SUBMISSION_COMPLETE'], allowedRoles: ['user'], estimatedDuration: 5, fields: [], @@ -215,15 +215,15 @@ export const createClaimWorkflowStepExamples = { value: { stepKey: 'CAPTURE_PART_DAMAGES', type: 'CLAIM', - stepNumber: 5, + stepNumber: 4, isActive: true, stepName_fa: 'ثبت تصاویر آسیب‌های قطعات', stepName_en: 'Capture Part Damages', description_fa: 'ثبت تصاویر آسیب برای هر قطعه (یک درخواست به ازای هر قطعه)', description_en: 'Capture damage images for each part (one request per part)', category: 'CLAIM', - requiredPreviousSteps: ['UPLOAD_REQUIRED_DOCUMENTS'], - nextPossibleSteps: ['USER_SUBMISSION_COMPLETE'], + requiredPreviousSteps: ['SELECT_OTHER_PARTS'], + nextPossibleSteps: ['UPLOAD_REQUIRED_DOCUMENTS'], allowedRoles: ['user'], estimatedDuration: 10, fields: [], @@ -251,7 +251,7 @@ export const createClaimWorkflowStepExamples = { description_fa: 'اطلاعات کاربر به طور کامل ثبت شد و در انتظار بررسی کارشناس خسارت', description_en: 'User information completed, waiting for damage expert review', category: 'CLAIM', - requiredPreviousSteps: ['CAPTURE_PART_DAMAGES'], + requiredPreviousSteps: ['UPLOAD_REQUIRED_DOCUMENTS'], nextPossibleSteps: ['EXPERT_DAMAGE_ASSESSMENT'], allowedRoles: ['user', 'damage_expert', 'admin'], estimatedDuration: 0, diff --git a/src/workflow-step-management/dto/workflow-step.examples.ts b/src/workflow-step-management/dto/workflow-step.examples.ts index 7c1d769..f5d58c1 100644 --- a/src/workflow-step-management/dto/workflow-step.examples.ts +++ b/src/workflow-step-management/dto/workflow-step.examples.ts @@ -1158,7 +1158,7 @@ export const claimWorkflowStepExamples = { "description_en": "Select other damaged parts (non-body), Sheba number and insurer national code", "category": "CLAIM", "requiredPreviousSteps": ["SELECT_OUTER_PARTS"], - "nextPossibleSteps": ["UPLOAD_REQUIRED_DOCUMENTS"], + "nextPossibleSteps": ["CAPTURE_PART_DAMAGES"], "allowedRoles": ["user"], "estimatedDuration": 4, "fields": [ @@ -1240,19 +1240,19 @@ export const claimWorkflowStepExamples = { }, uploadRequiredDocuments: { - summary: 'Claim Step 4: UPLOAD_REQUIRED_DOCUMENTS', + summary: 'Claim Step 5: UPLOAD_REQUIRED_DOCUMENTS', value: { "stepKey": "UPLOAD_REQUIRED_DOCUMENTS", "type": "CLAIM", - "stepNumber": 4, + "stepNumber": 5, "isActive": true, "stepName_fa": "آپلود مدارک مورد نیاز", "stepName_en": "Upload Required Documents", "description_fa": "آپلود مدارک مورد نیاز برای بررسی خسارت (لیست داینامیک بر اساس قطعات انتخاب شده)", "description_en": "Upload required documents for claim review (dynamic list based on selected parts)", "category": "CLAIM", - "requiredPreviousSteps": ["SELECT_OTHER_PARTS"], - "nextPossibleSteps": ["CAPTURE_PART_DAMAGES"], + "requiredPreviousSteps": ["CAPTURE_PART_DAMAGES"], + "nextPossibleSteps": ["USER_SUBMISSION_COMPLETE"], "allowedRoles": ["user"], "estimatedDuration": 5, "fields": [], @@ -1266,19 +1266,19 @@ export const claimWorkflowStepExamples = { }, capturePartDamages: { - summary: 'Claim Step 5: CAPTURE_PART_DAMAGES', + summary: 'Claim Step 4: CAPTURE_PART_DAMAGES', value: { "stepKey": "CAPTURE_PART_DAMAGES", "type": "CLAIM", - "stepNumber": 5, + "stepNumber": 4, "isActive": true, "stepName_fa": "ثبت تصاویر آسیب‌های قطعات", "stepName_en": "Capture Part Damages", "description_fa": "ثبت تصاویر آسیب برای هر قطعه (یک درخواست به ازای هر قطعه)", "description_en": "Capture damage images for each part (one request per part)", "category": "CLAIM", - "requiredPreviousSteps": ["UPLOAD_REQUIRED_DOCUMENTS"], - "nextPossibleSteps": ["USER_SUBMISSION_COMPLETE"], + "requiredPreviousSteps": ["SELECT_OTHER_PARTS"], + "nextPossibleSteps": ["UPLOAD_REQUIRED_DOCUMENTS"], "allowedRoles": ["user"], "estimatedDuration": 10, "fields": [], @@ -1305,7 +1305,7 @@ export const claimWorkflowStepExamples = { "description_fa": "اطلاعات کاربر به طور کامل ثبت شد و در انتظار بررسی کارشناس خسارت", "description_en": "User information completed, waiting for damage expert review", "category": "CLAIM", - "requiredPreviousSteps": ["CAPTURE_PART_DAMAGES"], + "requiredPreviousSteps": ["UPLOAD_REQUIRED_DOCUMENTS"], "nextPossibleSteps": ["EXPERT_DAMAGE_ASSESSMENT"], "allowedRoles": ["user", "damage_expert", "admin"], "estimatedDuration": 0,