1
0
forked from Yara724/api

Merge pull request 'Fixed statuses and switched some' (#23) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#23
This commit is contained in:
2026-04-18 17:42:27 +03:30
15 changed files with 455 additions and 112 deletions

View File

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

View File

@@ -674,7 +674,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: {
@@ -764,9 +764,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: {
@@ -3355,6 +3355,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: [
{
@@ -3476,6 +3483,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: [
{
@@ -3611,6 +3623,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: [
@@ -3721,7 +3738,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: {
@@ -3757,7 +3774,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) {
@@ -3875,9 +3892,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: {
@@ -3956,9 +3973,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;
@@ -4025,7 +4043,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' },
@@ -4207,11 +4224,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,
{
@@ -4227,15 +4247,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 {
@@ -4243,7 +4278,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) {
@@ -4340,10 +4377,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: {
@@ -4355,8 +4391,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.',
},
},
},
@@ -4365,7 +4402,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 {
@@ -4375,7 +4412,7 @@ export class ClaimRequestManagementService {
fileUrl,
allCapturesComplete,
currentStep: allCapturesComplete
? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
message,
};

View File

@@ -360,8 +360,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({
@@ -461,6 +461,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({
@@ -523,9 +525,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
@@ -534,7 +536,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.
`,
@@ -552,7 +554,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",
@@ -566,7 +567,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",
@@ -632,15 +633,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.
`,

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -669,33 +669,22 @@ export class ExpertBlameService {
);
}
const parties = (doc.parties ?? []);
const parties = Array.isArray(doc.parties) ? doc.parties : [];
const typedParties = parties as Array<{
vehicle?: {
inquiry?: {
mapped?: any; // Use 'any' or a more specific type if known
// other inquiry properties
mapped?: any;
};
// other vehicle properties
};
evidence?: { // For the second part of your original error description
evidence?: {
videoId?: string | number;
voices?: (string | number)[];
videoUrl?: string;
voiceUrls?: string[];
};
// other party properties
}>;
// Extract inquiry.mapped for each party if needed, but we will keep the full vehicle object.
for (const party of typedParties) {
const mapped = party?.vehicle?.inquiry?.mapped;
// If you still want this field, you can add it like:
// party.inquiryMapped = mapped ?? null;
// But based on your last request, we are keeping the full vehicle object.
}
// Evidence (videos + voices)
for (const party of typedParties) {
if (!party.evidence) continue;
@@ -727,7 +716,13 @@ export class ExpertBlameService {
doc.createdAtFormatted = `${createdDate} ${createdTime}`;
doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`;
// The line `delete party.vehicle;` has been removed, so vehicle will be included.
// Omit heavy SandHub inquiry blob from expert response (keep other vehicle fields)
for (const party of typedParties) {
const veh = party?.vehicle as Record<string, unknown> | undefined;
if (veh && Object.prototype.hasOwnProperty.call(veh, "inquiry")) {
delete veh.inquiry;
}
}
return doc;
} catch (error) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -39,6 +39,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";
@@ -172,6 +174,117 @@ export class ExpertClaimService {
private readonly branchDbService: BranchDbService,
) { }
/** 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<string, any>,
):
| { 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;
@@ -2237,36 +2350,62 @@ export class ExpertClaimService {
],
});
const list = (claims as any[])
.filter((c) => claimCaseTouchesClient(c, clientKey))
.map((c) => {
const awaitingFactorValidation =
c.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
c.claimStatus === ClaimStatus.UNDER_REVIEW &&
c.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION;
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: c.vehicle
? {
carName: c.vehicle.carName,
carModel: c.vehicle.carModel,
carType: c.vehicle.carType,
}
: undefined,
createdAt: c.createdAt,
awaitingFactorValidation,
};
}) 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<string, any>(
blames.map((b) => [String(b._id), b]),
);
const list = filtered.map((c) => {
const awaitingFactorValidation =
c.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
c.claimStatus === ClaimStatus.UNDER_REVIEW &&
c.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION;
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,
awaitingFactorValidation,
};
}) as ClaimListItemV2Dto[];
return { list, total: list.length };
}
@@ -2360,6 +2499,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,
@@ -2381,7 +2546,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,

View File

@@ -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;
}
/**

View File

@@ -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;
}

View File

@@ -307,11 +307,11 @@ 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,
private readonly userAuthService: UserAuthService,
private readonly claimCaseDbService: ClaimCaseDbService,
) { }
/**
@@ -3982,6 +3982,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.
@@ -4024,7 +4099,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 };
}
/**

View File

@@ -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(

View File

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

View File

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