1
0
forked from Yara724/api

update some important issues

This commit is contained in:
2026-04-18 10:49:22 +03:30
parent 494e3d93ab
commit 4bdb9fd469
22 changed files with 1727 additions and 105 deletions

View File

@@ -32,6 +32,7 @@ import { MyRequestsDto } from "./dto/my-request.dto";
import { SubmitUserReplyDtoRs } from "./dto/submit-reply.dto";
import { UserCommentDto } from "./dto/user-comment.dto";
import { UserObjectionDto } from "./dto/user-objection.dto";
import { UserObjectionV2Dto } from "./dto/user-objection-v2.dto";
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
@@ -41,7 +42,11 @@ import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto } from "./dto/sele
import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto";
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto";
import { CapturePartV2Dto, CapturePartV2ResponseDto } from "./dto/capture-part-v2.dto";
import {
CapturePartV2Dto,
CapturePartV2ResponseDto,
VideoCaptureV2ResponseDto,
} from "./dto/capture-part-v2.dto";
import { GetMyClaimsV2ResponseDto, ClaimListItemV2Dto } from "./dto/my-claims-v2.dto";
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
@@ -1818,6 +1823,172 @@ export class ClaimRequestManagementService {
}
}
/**
* V2: Upload repair factor file for a part marked `factorNeeded` on the expert reply (ClaimCase).
* Same behavior as v1 {@link uploadClaimFactor} but uses `evaluation.damageExpertReply` / `damageExpertReplyFinal`.
*/
async uploadClaimFactorV2(
claimRequestId: string,
partId: string,
file: Express.Multer.File,
currentUserId: string,
actor?: { sub: string; role?: string },
) {
try {
if (!file) {
throw new BadRequestException("A factor file is required for upload.");
}
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException("Claim not found");
}
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 file");
}
const replyKey = claim.evaluation?.damageExpertReplyFinal
? "damageExpertReplyFinal"
: "damageExpertReply";
const finalReply =
replyKey === "damageExpertReplyFinal"
? claim.evaluation?.damageExpertReplyFinal
: claim.evaluation?.damageExpertReply;
if (!finalReply?.parts?.length) {
throw new BadRequestException("No expert reply found in this claim.");
}
const part = finalReply.parts.find((p) => String(p.partId) === String(partId));
if (!part) {
throw new BadRequestException(
"The specified part was not found in the expert's reply.",
);
}
if (!part.factorNeeded) {
throw new BadRequestException("This part does not require a factor upload.");
}
if (part.factorLink) {
throw new ConflictException(
"A factor has already been uploaded for this part.",
);
}
if (claim.claimStatus !== ClaimStatus.NEEDS_REVISION) {
throw new BadRequestException(
"Factors can only be uploaded while the claim is in NEEDS_REVISION (expert requested factor documents). After all factors are uploaded the claim moves to UNDER_REVIEW for expert validation only.",
);
}
const partNameForRecord = part.carPartDamage ?? part.partId;
const factorRecord = await this.claimFactorsImageDbService.create({
claimId: new Types.ObjectId(claimRequestId),
partId: String(partId),
partName: partNameForRecord,
path: file.path,
fileName: file.filename,
uploadedAt: new Date(),
});
const pathPrefix = `evaluation.${replyKey}.parts`;
const updatedClaim = await this.claimCaseDbService.findOneAndUpdate(
{
_id: new Types.ObjectId(claimRequestId),
[`${pathPrefix}.partId`]: partId,
},
{
$set: {
[`${pathPrefix}.$.factorLink`]: factorRecord._id,
[`${pathPrefix}.$.factorStatus`]: FactorStatus.PENDING,
},
},
);
if (!updatedClaim) {
throw new NotFoundException(
"Could not find the specific part to update in the database.",
);
}
await this.checkAndTransitionStatusAfterFactorUploadV2(claimRequestId);
return {
message: "Factor uploaded successfully. Awaiting expert validation.",
factorId: factorRecord._id,
url: buildFileLink(file.path),
};
} catch (error) {
this.logger.error(
`Error during V2 factor upload for claim ${claimRequestId}:`,
error instanceof Error ? error.stack : String(error),
);
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
"An internal server error occurred during factor upload.",
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* When every `factorNeeded` part on the active expert reply has a `factorLink`, move claim toward expert validation (v1: PendingFactorValidation).
*/
private async checkAndTransitionStatusAfterFactorUploadV2(
claimRequestId: string,
): Promise<void> {
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim?.evaluation) {
return;
}
const finalReply =
claim.evaluation.damageExpertReplyFinal ||
claim.evaluation.damageExpertReply;
if (!finalReply?.parts) {
return;
}
const requiredFactorParts = finalReply.parts.filter((p) => p.factorNeeded === true);
if (requiredFactorParts.length === 0) {
return;
}
const allFactorsAreUploaded = requiredFactorParts.every((p) => !!p.factorLink);
if (!allFactorsAreUploaded) {
return;
}
this.logger.log(
`All required factors for claim case ${claimRequestId} are uploaded (V2). Setting claim status for validation.`,
);
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { claimStatus: ClaimStatus.UNDER_REVIEW },
$push: {
history: {
type: "ALL_FACTORS_UPLOADED_PENDING_VALIDATION",
timestamp: new Date(),
metadata: { claimRequestId },
},
},
});
}
private async checkAndTransitionStatusAfterUpload(
claimId: string,
): Promise<void> {
@@ -3783,6 +3954,288 @@ export class ClaimRequestManagementService {
}
}
/**
* V2 API: Upload car walk-around video (same storage as v1: claim-video-capture + ClaimCase.media.videoCaptureId).
*/
async setVideoCaptureV2(
claimRequestId: string,
fileDetail: Express.Multer.File,
currentUserId: string,
actor?: { sub: string; role?: string },
): Promise<VideoCaptureV2ResponseDto> {
try {
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
if (!claimCase) {
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
}
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 car capture video');
}
if (!fileDetail) {
throw new BadRequestException('Video file is required.');
}
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.CAPTURE_PART_DAMAGES) {
throw new BadRequestException(
`Invalid workflow step. Expected ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES}, but current step is ${claimCase.workflow?.currentStep}`,
);
}
if (claimCase.media?.videoCaptureId) {
throw new ConflictException('A video has already been uploaded for this claim.');
}
const videoDoc = await this.createVideoCapture(fileDetail, claimRequestId);
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { 'media.videoCaptureId': videoDoc._id },
$push: {
history: {
type: 'VIDEO_CAPTURE_UPLOADED',
actor: {
actorId: new Types.ObjectId(currentUserId),
actorName: claimCase.owner?.fullName || 'User',
actorType: 'user',
},
timestamp: new Date(),
metadata: {
videoId: videoDoc._id.toString(),
fileName: fileDetail.filename,
},
},
},
});
return {
claimRequestId: claimCase._id.toString(),
videoId: videoDoc._id.toString(),
message: 'Video capture uploaded successfully.',
};
} catch (error) {
if (error instanceof HttpException) throw error;
this.logger.error('setVideoCaptureV2 failed', error);
throw new InternalServerErrorException('Failed to upload video capture');
}
}
/**
* Whether an expert resend request exists (user is expected to respond).
*/
private claimCaseHasExpertResend(claimCase: any): boolean {
const r = claimCase?.evaluation?.damageExpertResend;
if (!r) return false;
if (typeof r.resendDescription === 'string' && r.resendDescription.trim() !== '') {
return true;
}
if (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0) return true;
if (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0) return true;
return false;
}
/**
* V2 API: User objection (ClaimCase + evaluation.objection payload).
*
* Preconditions: damage expert resend and/or first expert reply exists; no prior objection; no final reply yet.
* Post: claim returns to expert queue (WAITING_FOR_DAMAGE_EXPERT), objection stored, optional new parts merged into damage.selectedParts.
*/
async handleUserObjectionV2(
claimRequestId: string,
body: UserObjectionV2Dto,
currentUserId: string,
actor?: { sub: string; role?: string },
) {
try {
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
if (!claimCase) {
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
}
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 submit an objection');
}
const hasExpertResend = this.claimCaseHasExpertResend(claimCase);
const hasFirstExpertReply = !!claimCase.evaluation?.damageExpertReply;
const hasFinalExpertReply = !!claimCase.evaluation?.damageExpertReplyFinal;
if (!hasExpertResend && !hasFirstExpertReply) {
throw new BadRequestException(
'You can only object after the damage expert has submitted an assessment or requested a resend.',
);
}
if (hasFinalExpertReply) {
throw new ConflictException(
'The expert has already submitted the final reply after objection; you cannot submit another objection through this endpoint.',
);
}
if (claimCase.evaluation?.objection?.submittedAt) {
throw new ConflictException('An objection has already been submitted for this claim.');
}
const partsIn = body.objectionParts ?? [];
const newPartsIn = body.newParts ?? [];
if (partsIn.length === 0 && newPartsIn.length === 0) {
throw new BadRequestException(
'Provide at least one entry in objectionParts and/or newParts.',
);
}
const objectionParts = partsIn.map((p) => ({
partId: p.partId,
reason: p.reason,
partPrice: p.partPrice,
partSalary: p.partSalary,
typeOfDamage: p.typeOfDamage != null ? String(p.typeOfDamage) : undefined,
carPartDamage: p.carPartDamage,
side: p.side,
}));
const newParts = newPartsIn.map((p) => ({
partId: p.partId ? String(p.partId) : new Types.ObjectId().toString(),
partName: p.partName,
side: p.side,
}));
const mergedSelected = [...(claimCase.damage?.selectedParts ?? [])];
for (const np of newParts) {
const key = np.partName?.trim();
if (key && !mergedSelected.includes(key)) {
mergedSelected.push(key);
}
}
const objectionPayload = {
objectionParts,
newParts,
submittedAt: new Date(),
};
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: {
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
claimStatus: ClaimStatus.PENDING,
'workflow.locked': false,
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
'workflow.nextStep': ClaimWorkflowStep.EXPERT_COST_EVALUATION,
'evaluation.objection': objectionPayload,
'damage.selectedParts': mergedSelected,
},
$push: {
history: {
type: 'USER_OBJECTION_SUBMITTED',
actor: {
actorId: new Types.ObjectId(effectiveUserId),
actorName: claimCase.owner?.fullName || 'User',
actorType: 'user',
},
timestamp: new Date(),
metadata: {
objectionPartCount: objectionParts.length,
newPartCount: newParts.length,
},
},
},
});
return objectionPayload;
} catch (error) {
if (error instanceof HttpException) throw error;
this.logger.error('handleUserObjectionV2 failed', error);
throw new InternalServerErrorException('Failed to submit objection');
}
}
/**
* V2 API: User satisfaction rating for a completed claim case (same intent as v1 PUT …/request/:id/user-rating).
*/
async addUserRatingV2(
claimRequestId: string,
ratingDto: UserRatingDto,
currentUserId: string,
actor?: { sub: string; role?: string },
): Promise<UserClaimRating> {
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
if (!claimCase) {
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
}
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 submit a satisfaction rating.',
);
}
if (claimCase.status !== ClaimCaseStatus.COMPLETED) {
throw new BadRequestException(
'You can only rate a claim after it has been completed.',
);
}
if (claimCase.userRating?.createdAt) {
throw new ConflictException('A rating has already been submitted for this claim.');
}
const score = (n: number) => Number(n);
for (const [key, label] of [
[ratingDto.progressSpeed, 'progressSpeed'],
[ratingDto.registrationEase, 'registrationEase'],
[ratingDto.overallEvaluation, 'overallEvaluation'],
] as const) {
const v = score(key);
if (Number.isNaN(v) || v < 0 || v > 5) {
throw new BadRequestException(`${label} must be a number between 0 and 5.`);
}
}
const userRating: UserClaimRating = {
progressSpeed: ratingDto.progressSpeed,
registrationEase: ratingDto.registrationEase,
overallEvaluation: ratingDto.overallEvaluation,
comment: ratingDto.comment,
createdAt: new Date(),
};
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { userRating },
$push: {
history: {
type: 'USER_RATING_SUBMITTED',
actor: {
actorId: new Types.ObjectId(effectiveUserId),
actorName: claimCase.owner?.fullName || 'User',
actorType: 'user',
},
timestamp: new Date(),
metadata: {},
},
},
});
return userRating;
}
/**
* V2 API: Get list of claims for current user (or for FIELD_EXPERT: claims from their expert-initiated IN_PERSON blame files).
*/
@@ -3929,6 +4382,15 @@ export class ClaimRequestManagementService {
requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 ? requiredDocumentsStatus : undefined,
carAngles,
damagedParts,
userRating: claim.userRating
? {
progressSpeed: claim.userRating.progressSpeed,
registrationEase: claim.userRating.registrationEase,
overallEvaluation: claim.userRating.overallEvaluation,
comment: claim.userRating.comment,
createdAt: claim.userRating.createdAt,
}
: undefined,
createdAt: (claim as any).createdAt,
updatedAt: (claim as any).updatedAt,
};