forked from Yara724/api
Added Field Expert flows, and deactivated inquiry
This commit is contained in:
@@ -2880,6 +2880,141 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Field expert creates a claim from an expert-initiated IN_PERSON completed blame.
|
||||
* The claim owner is set to the damaged party (first for CAR_BODY, non-guilty for THIRD_PARTY).
|
||||
*/
|
||||
async createClaimFromBlameForExpertV2(
|
||||
blameRequestId: string,
|
||||
expert: { sub: string; firstName?: string; lastName?: string },
|
||||
): Promise<CreateClaimFromBlameResponseDto> {
|
||||
const blameRequest = await this.blameRequestDbService.findById(blameRequestId);
|
||||
if (!blameRequest) {
|
||||
throw new NotFoundException("Blame request not found");
|
||||
}
|
||||
if (blameRequest.status !== BlameCaseStatus.COMPLETED) {
|
||||
throw new BadRequestException(
|
||||
`Blame request must be COMPLETED. Current status: ${blameRequest.status}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!blameRequest.expertInitiated ||
|
||||
blameRequest.creationMethod !== "IN_PERSON" ||
|
||||
!blameRequest.initiatedByFieldExpertId
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
if (String(blameRequest.initiatedByFieldExpertId) !== String(expert.sub)) {
|
||||
throw new ForbiddenException(
|
||||
"Only the field expert who created this blame file can create the claim.",
|
||||
);
|
||||
}
|
||||
const parties = blameRequest.parties || [];
|
||||
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
||||
let damagedUserId: string;
|
||||
if (isCarBody) {
|
||||
if (parties.length < 1) throw new BadRequestException("Blame request has no party");
|
||||
const first = parties.find((p) => p.role === "FIRST");
|
||||
if (!first?.person?.userId) throw new BadRequestException("First party has no userId");
|
||||
damagedUserId = String(first.person.userId);
|
||||
} else {
|
||||
const guiltyPartyId = blameRequest.expert?.decision?.guiltyPartyId
|
||||
? String(blameRequest.expert.decision.guiltyPartyId)
|
||||
: null;
|
||||
if (!guiltyPartyId) throw new BadRequestException("Blame request has no guilty party set");
|
||||
const damagedParty = parties.find(
|
||||
(p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId,
|
||||
);
|
||||
if (!damagedParty?.person?.userId) {
|
||||
throw new BadRequestException("Could not determine damaged party");
|
||||
}
|
||||
damagedUserId = String(damagedParty.person.userId);
|
||||
}
|
||||
const existingClaim = await this.claimCaseDbService.findOne({
|
||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||
});
|
||||
if (existingClaim) {
|
||||
throw new ConflictException("A claim for this blame case already exists");
|
||||
}
|
||||
const claimNo = await this.generateUniqueClaimNumber();
|
||||
const damagedParty = parties.find(
|
||||
(p) => p.person?.userId && String(p.person.userId) === damagedUserId,
|
||||
);
|
||||
const newClaim = await this.claimCaseDbService.create({
|
||||
requestNo: claimNo,
|
||||
publicId: blameRequest.publicId,
|
||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||
blameRequestNo: blameRequest.requestNo,
|
||||
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||
claimStatus: ClaimStatus.PENDING,
|
||||
workflow: {
|
||||
currentStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
nextStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||
locked: false,
|
||||
},
|
||||
owner: {
|
||||
userId: new Types.ObjectId(damagedUserId),
|
||||
userRole: damagedParty?.role as any,
|
||||
},
|
||||
history: [
|
||||
{
|
||||
type: "CLAIM_CREATED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
blameRequestId,
|
||||
blamePublicId: blameRequest.publicId,
|
||||
createdByExpert: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
this.logger.log(
|
||||
`Claim created by field expert: ${newClaim._id} from blame: ${blameRequestId}`,
|
||||
);
|
||||
return {
|
||||
claimRequestId: String(newClaim._id),
|
||||
publicId: blameRequest.publicId,
|
||||
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||
message: "Claim created successfully. You can now fill claim data on behalf of the damaged party.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve effective user id for claim operations.
|
||||
* For FIELD_EXPERT acting on expert-initiated IN_PERSON claim, returns the claim owner's id; otherwise returns currentUserId.
|
||||
*/
|
||||
private async resolveClaimEffectiveUserId(
|
||||
claimCase: any,
|
||||
currentUserId: string,
|
||||
actorRole?: string,
|
||||
): Promise<string> {
|
||||
if (actorRole !== RoleEnum.FIELD_EXPERT || !claimCase?.blameRequestId) {
|
||||
return currentUserId;
|
||||
}
|
||||
const blameRequest = await this.blameRequestDbService.findById(
|
||||
claimCase.blameRequestId.toString(),
|
||||
);
|
||||
if (
|
||||
!blameRequest?.expertInitiated ||
|
||||
blameRequest.creationMethod !== "IN_PERSON" ||
|
||||
!blameRequest.initiatedByFieldExpertId
|
||||
) {
|
||||
return currentUserId;
|
||||
}
|
||||
if (String(blameRequest.initiatedByFieldExpertId) !== currentUserId) {
|
||||
return currentUserId;
|
||||
}
|
||||
return claimCase.owner?.userId ? String(claimCase.owner.userId) : currentUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Select damaged outer car parts for claim (Step 2 of claim workflow)
|
||||
*
|
||||
@@ -2899,6 +3034,7 @@ export class ClaimRequestManagementService {
|
||||
claimRequestId: string,
|
||||
body: SelectOuterPartsV2Dto,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||
try {
|
||||
// 1. Validate claim exists
|
||||
@@ -2910,8 +3046,13 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Validate user is the claim owner (damaged party)
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
claimCase,
|
||||
currentUserId,
|
||||
actor?.role,
|
||||
);
|
||||
// 2. Validate user is the claim owner (or field expert acting for IN_PERSON claim)
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||
throw new ForbiddenException(
|
||||
'Only the claim owner (damaged party) can select damaged parts'
|
||||
);
|
||||
@@ -3013,6 +3154,7 @@ export class ClaimRequestManagementService {
|
||||
claimRequestId: string,
|
||||
body: SelectOtherPartsV2Dto,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||
try {
|
||||
// 1. Validate claim exists
|
||||
@@ -3024,8 +3166,13 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Validate user is the claim owner (damaged party)
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
claimCase,
|
||||
currentUserId,
|
||||
actor?.role,
|
||||
);
|
||||
// 2. Validate user is the claim owner (or field expert for IN_PERSON claim)
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||
throw new ForbiddenException(
|
||||
'Only the claim owner (damaged party) can submit other parts and bank information'
|
||||
);
|
||||
@@ -3126,6 +3273,7 @@ export class ClaimRequestManagementService {
|
||||
async getCaptureRequirementsV2(
|
||||
claimRequestId: string,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -3134,7 +3282,12 @@ export class ClaimRequestManagementService {
|
||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||
}
|
||||
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
claimCase,
|
||||
currentUserId,
|
||||
actor?.role,
|
||||
);
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||
throw new ForbiddenException('Only the claim owner can view capture requirements');
|
||||
}
|
||||
|
||||
@@ -3158,7 +3311,11 @@ export class ClaimRequestManagementService {
|
||||
return labels[key] || { fa: key, en: key };
|
||||
};
|
||||
|
||||
// Required documents
|
||||
// Required documents: CAR_BODY needs 7 (damaged + green card); THIRD_PARTY needs 13 (add guilty_*)
|
||||
const blameRequest = claimCase.blameRequestId
|
||||
? await this.blameRequestDbService.findById(claimCase.blameRequestId.toString())
|
||||
: 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' },
|
||||
@@ -3168,11 +3325,13 @@ export class ClaimRequestManagementService {
|
||||
{ key: 'damaged_car_card_front', label_fa: 'کارت خودرو آسیبدیده - جلو', label_en: 'Damaged Car Card - Front', category: 'damaged_party' },
|
||||
{ key: 'damaged_car_card_back', label_fa: 'کارت خودرو آسیبدیده - پشت', label_en: 'Damaged Car Card - Back', category: 'damaged_party' },
|
||||
{ key: 'damaged_metal_plate', label_fa: 'پلاک فلزی آسیبدیده', label_en: 'Damaged Metal Plate', category: 'damaged_party' },
|
||||
{ key: 'guilty_driving_license_front', label_fa: 'گواهینامه طرف مقصر - جلو', label_en: 'Guilty Party License - Front', category: 'guilty_party' },
|
||||
{ key: 'guilty_driving_license_back', label_fa: 'گواهینامه طرف مقصر - پشت', label_en: 'Guilty Party License - Back', category: 'guilty_party' },
|
||||
{ key: 'guilty_car_card_front', label_fa: 'کارت خودرو مقصر - جلو', label_en: 'Guilty Car Card - Front', category: 'guilty_party' },
|
||||
{ key: 'guilty_car_card_back', label_fa: 'کارت خودرو مقصر - پشت', label_en: 'Guilty Car Card - Back', category: 'guilty_party' },
|
||||
{ key: 'guilty_metal_plate', label_fa: 'پلاک فلزی مقصر', label_en: 'Guilty Metal Plate', category: 'guilty_party' },
|
||||
...(isCarBody ? [] : [
|
||||
{ key: 'guilty_driving_license_front', label_fa: 'گواهینامه طرف مقصر - جلو', label_en: 'Guilty Party License - Front', category: 'guilty_party' },
|
||||
{ key: 'guilty_driving_license_back', label_fa: 'گواهینامه طرف مقصر - پشت', label_en: 'Guilty Party License - Back', category: 'guilty_party' },
|
||||
{ key: 'guilty_car_card_front', label_fa: 'کارت خودرو مقصر - جلو', label_en: 'Guilty Car Card - Front', category: 'guilty_party' },
|
||||
{ key: 'guilty_car_card_back', label_fa: 'کارت خودرو مقصر - پشت', label_en: 'Guilty Car Card - Back', category: 'guilty_party' },
|
||||
{ key: 'guilty_metal_plate', label_fa: 'پلاک فلزی مقصر', label_en: 'Guilty Metal Plate', category: 'guilty_party' },
|
||||
]),
|
||||
];
|
||||
|
||||
const requiredDocuments = requiredDocsDefinition.map(doc => {
|
||||
@@ -3256,6 +3415,7 @@ export class ClaimRequestManagementService {
|
||||
body: UploadRequiredDocumentV2Dto,
|
||||
file: Express.Multer.File,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -3264,7 +3424,12 @@ export class ClaimRequestManagementService {
|
||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||
}
|
||||
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
claimCase,
|
||||
currentUserId,
|
||||
actor?.role,
|
||||
);
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||
throw new ForbiddenException('Only the claim owner can upload documents');
|
||||
}
|
||||
|
||||
@@ -3274,6 +3439,16 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// CAR_BODY claims only allow 7 document types (no guilty_*)
|
||||
const blameForUpload = claimCase.blameRequestId
|
||||
? await this.blameRequestDbService.findById(claimCase.blameRequestId.toString())
|
||||
: null;
|
||||
const isCarBodyUpload = blameForUpload?.type === BlameRequestType.CAR_BODY;
|
||||
const guiltyKeys = ['guilty_driving_license_front', 'guilty_driving_license_back', 'guilty_car_card_front', 'guilty_car_card_back', 'guilty_metal_plate'];
|
||||
if (isCarBodyUpload && guiltyKeys.includes(body.documentKey)) {
|
||||
throw new BadRequestException(`Document type ${body.documentKey} is not required for CAR_BODY claims`);
|
||||
}
|
||||
|
||||
// Check if document already uploaded
|
||||
const existingDoc = (claimCase.requiredDocuments as any)?.get?.(body.documentKey);
|
||||
if (existingDoc?.uploaded) {
|
||||
@@ -3318,11 +3493,11 @@ export class ClaimRequestManagementService {
|
||||
},
|
||||
};
|
||||
|
||||
// Check if all documents are uploaded
|
||||
const totalDocs = 13;
|
||||
// Check if all documents are uploaded (7 for CAR_BODY, 13 for THIRD_PARTY)
|
||||
const totalDocsRequired = isCarBodyUpload ? 7 : 13;
|
||||
const currentDocs = claimCase.requiredDocuments || new Map();
|
||||
const uploadedCount = (currentDocs instanceof Map ? currentDocs.size : Object.keys(currentDocs).length) + 1;
|
||||
const allDocumentsUploaded = uploadedCount >= totalDocs;
|
||||
const allDocumentsUploaded = uploadedCount >= totalDocsRequired;
|
||||
|
||||
// If all documents uploaded, move to next step
|
||||
if (allDocumentsUploaded) {
|
||||
@@ -3350,7 +3525,7 @@ export class ClaimRequestManagementService {
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updateData);
|
||||
|
||||
const remaining = totalDocs - uploadedCount;
|
||||
const remaining = totalDocsRequired - uploadedCount;
|
||||
const message = allDocumentsUploaded
|
||||
? 'All documents uploaded successfully. Please proceed to capture car angles and damaged parts.'
|
||||
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||
@@ -3378,6 +3553,7 @@ export class ClaimRequestManagementService {
|
||||
body: CapturePartV2Dto,
|
||||
file: Express.Multer.File,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<CapturePartV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -3386,7 +3562,12 @@ export class ClaimRequestManagementService {
|
||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||
}
|
||||
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
claimCase,
|
||||
currentUserId,
|
||||
actor?.role,
|
||||
);
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||
throw new ForbiddenException('Only the claim owner can capture parts');
|
||||
}
|
||||
|
||||
@@ -3498,14 +3679,42 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Get list of claims for current user
|
||||
* V2 API: Get list of claims for current user (or for FIELD_EXPERT: claims from their expert-initiated IN_PERSON blame files).
|
||||
*/
|
||||
async getMyClaimsV2(currentUserId: string): Promise<GetMyClaimsV2ResponseDto> {
|
||||
async getMyClaimsV2(
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<GetMyClaimsV2ResponseDto> {
|
||||
try {
|
||||
const claims = await this.claimCaseDbService.find(
|
||||
{ 'owner.userId': new Types.ObjectId(currentUserId) },
|
||||
{ lean: true },
|
||||
);
|
||||
let claims: any[];
|
||||
if (actor?.role === RoleEnum.FIELD_EXPERT) {
|
||||
const expertBlameIds = await this.blameRequestDbService
|
||||
.find(
|
||||
{
|
||||
expertInitiated: true,
|
||||
creationMethod: 'IN_PERSON',
|
||||
initiatedByFieldExpertId: new Types.ObjectId(currentUserId),
|
||||
},
|
||||
{ select: '_id', lean: true },
|
||||
)
|
||||
.then((docs) => docs.map((d) => (d as any)._id));
|
||||
claims = await this.claimCaseDbService.find(
|
||||
{
|
||||
$or: [
|
||||
{ 'owner.userId': new Types.ObjectId(currentUserId) },
|
||||
...(expertBlameIds.length
|
||||
? [{ blameRequestId: { $in: expertBlameIds } }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{ lean: true },
|
||||
);
|
||||
} else {
|
||||
claims = await this.claimCaseDbService.find(
|
||||
{ 'owner.userId': new Types.ObjectId(currentUserId) },
|
||||
{ lean: true },
|
||||
);
|
||||
}
|
||||
const list = (claims as any[]).map((c) => ({
|
||||
claimRequestId: c._id.toString(),
|
||||
publicId: c.publicId,
|
||||
@@ -3530,13 +3739,19 @@ export class ClaimRequestManagementService {
|
||||
async getClaimDetailsV2(
|
||||
claimRequestId: string,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<ClaimDetailsV2ResponseDto> {
|
||||
try {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException('Claim request not found');
|
||||
}
|
||||
if (!claim.owner?.userId || claim.owner.userId.toString() !== currentUserId) {
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
claim,
|
||||
currentUserId,
|
||||
actor?.role,
|
||||
);
|
||||
if (!claim.owner?.userId || claim.owner.userId.toString() !== effectiveUserId) {
|
||||
throw new ForbiddenException('You do not have access to this claim');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user