resolved conflicts and maintained new features

This commit is contained in:
SepehrYahyaee
2026-04-18 17:41:45 +03:30
26 changed files with 2091 additions and 548 deletions

View File

@@ -26,6 +26,13 @@ import { RequestManagementDbService } from "src/request-management/entities/db-s
import { UserResendResponseDto, UserResendUploadResponseDto } from "./dto/user-resend.dto";
import { UserSignatureResponseDto } from "./dto/user-signature.dto";
import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum";
import {
buildResendItemsWithUi,
isResendPartyItemSatisfied,
} from "src/Types&Enums/blame-request-management/resend-item-ui";
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
import { SandHubService } from "src/sand-hub/sand-hub.service";
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
@@ -46,7 +53,6 @@ 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,
@@ -308,6 +314,64 @@ export class RequestManagementService {
private readonly userAuthService: UserAuthService,
) { }
/**
* Linked claim cases: block user on damage flow while blame awaits document resend.
*/
async applyLinkedClaimsBlameResendStarted(blameRequestId: string): Promise<void> {
const oid = new Types.ObjectId(blameRequestId);
const claims = await this.claimCaseDbService.find({
blameRequestId: oid,
});
for (const c of claims as any[]) {
const st = c.status as ClaimCaseStatus;
if (
st === ClaimCaseStatus.COMPLETED ||
st === ClaimCaseStatus.CANCELLED ||
st === ClaimCaseStatus.REJECTED
) {
continue;
}
await this.claimCaseDbService.findByIdAndUpdate(c._id, {
$set: {
blameDocumentResendPending: true,
claimStatus: ClaimStatus.NEEDS_REVISION,
},
$push: {
history: {
type: "BLAME_DOCUMENT_RESEND_STARTED",
timestamp: new Date(),
metadata: { blameRequestId },
},
},
});
}
}
async applyLinkedClaimsBlameResendCleared(blameRequestId: string): Promise<void> {
const oid = new Types.ObjectId(blameRequestId);
const claims = await this.claimCaseDbService.find({
blameRequestId: oid,
});
for (const c of claims as any[]) {
if (!c.blameDocumentResendPending) {
continue;
}
await this.claimCaseDbService.findByIdAndUpdate(c._id, {
$set: {
blameDocumentResendPending: false,
claimStatus: ClaimStatus.PENDING,
},
$push: {
history: {
type: "BLAME_DOCUMENT_RESEND_CLEARED",
timestamp: new Date(),
metadata: { blameRequestId },
},
},
});
}
}
async createRequestV2(user: any, type: BlameRequestType) {
const firstStep = await this.getWorkflowStep({ stepNumber: 1 });
if (!firstStep?.stepKey) {
@@ -5761,11 +5825,27 @@ export class RequestManagementService {
);
}
const items = (userResendRequest.requestedItems || []) as string[];
const row = userResendRequest as any;
const merged = {
uploadedDocuments: {
...(typeof row.uploadedDocuments === "object" && row.uploadedDocuments
? { ...row.uploadedDocuments }
: {}),
},
resendVoiceId: row.resendVoiceId,
resendVideoId: row.resendVideoId,
userTextDescription: row.userTextDescription,
};
const remainingItems = items.filter((i) => !isResendPartyItemSatisfied(i, merged));
return {
requestId: String(request._id),
requestedItems: userResendRequest.requestedItems,
description: userResendRequest.description || "",
completed: userResendRequest.completed || false,
requestedItemsUi: buildResendItemsWithUi(items),
remainingItems,
};
}
@@ -5783,6 +5863,7 @@ export class RequestManagementService {
voice?: Express.Multer.File[];
video?: Express.Multer.File[];
},
textDescription?: string,
): Promise<UserResendUploadResponseDto> {
const request = await this.blameRequestDbService.findById(requestId);
if (!request) {
@@ -5806,7 +5887,9 @@ export class RequestManagementService {
throw new ForbiddenException("You are not a party in this request");
}
const partyIndex = parties.findIndex((p) => String(p.person?.userId) === userId);
const partyIndex = parties.findIndex(
(p) => String(p.person?.userId) === String(userId),
);
if (partyIndex === -1) {
throw new ForbiddenException("Party not found in request");
}
@@ -5826,50 +5909,85 @@ export class RequestManagementService {
const userResendRequest = resendParties[resendIndex];
const requestedItems = userResendRequest.requestedItems || [];
// Validate uploaded files match requested items
const row = userResendRequest as any;
const mergedRow = {
uploadedDocuments: {
...(typeof row.uploadedDocuments === "object" && row.uploadedDocuments
? { ...row.uploadedDocuments }
: {}),
},
resendVoiceId: row.resendVoiceId,
resendVideoId: row.resendVideoId,
userTextDescription: row.userTextDescription,
};
const hasAnyFile = Object.keys(files || {}).some((fieldName) => {
const fileArray = files[fieldName as keyof typeof files];
return fileArray && fileArray.length > 0 && fileArray[0];
});
const descTrim =
textDescription != null ? String(textDescription).trim() : "";
const sendingDescription =
requestedItems.includes(ResendItemType.DESCRIPTION) && descTrim.length > 0;
if (!hasAnyFile && !sendingDescription) {
throw new BadRequestException(
"Provide at least one requested file or a text description when description was requested.",
);
}
const uploadedItems: string[] = [];
const updatePayload: any = { $set: {} };
if (sendingDescription) {
updatePayload.$set[
`expert.resend.parties.${resendIndex}.userTextDescription`
] = descTrim;
mergedRow.userTextDescription = descTrim;
uploadedItems.push(ResendItemType.DESCRIPTION);
}
for (const fieldName in files) {
const fileArray = files[fieldName];
const fileArray = files[fieldName as keyof typeof files];
if (!fileArray || fileArray.length === 0) continue;
const file = fileArray[0];
// Check if this item was requested
if (!requestedItems.includes(fieldName)) {
throw new BadRequestException(
`${fieldName} was not requested. Requested items: ${requestedItems.join(", ")}`,
);
}
// Handle different file types
if (fieldName === "voice") {
// Create voice record
const voiceDoc = await this.blameVoiceDbService.create({
path: file.path,
requestId: new Types.ObjectId(requestId),
fileName: file.filename,
context: "EXPERT_RESEND" as any,
});
// Add voice to party evidence
updatePayload.$addToSet = updatePayload.$addToSet || {};
updatePayload.$addToSet[`parties.${partyIndex}.evidence.voices`] =
(voiceDoc as any)._id;
updatePayload.$set[
`expert.resend.parties.${resendIndex}.resendVoiceId`
] = (voiceDoc as any)._id;
mergedRow.resendVoiceId = (voiceDoc as any)._id;
uploadedItems.push(fieldName);
} else if (fieldName === "video") {
// Create video record
const videoDoc = await this.blameVideoDbService.create({
path: file.path,
requestId: new Types.ObjectId(requestId),
fileName: file.filename,
});
// Set video in party evidence
updatePayload.$set[`parties.${partyIndex}.evidence.videoId`] =
String((videoDoc as any)._id);
updatePayload.$set[
`expert.resend.parties.${resendIndex}.resendVideoId`
] = (videoDoc as any)._id;
mergedRow.resendVideoId = (videoDoc as any)._id;
uploadedItems.push(fieldName);
} else {
// Document types (nationalCertificate, carCertificate, etc.)
const docType = fieldName as BlameDocumentType;
const doc = await this.blameDocumentDbService.create({
path: file.path,
@@ -5877,22 +5995,16 @@ export class RequestManagementService {
fileName: file.filename,
documentType: docType,
});
// Store document reference (can be stored in a resend-specific field or added to party documents)
// For now, store in expert.resend.parties[].uploadedDocuments
updatePayload.$set[
`expert.resend.parties.${resendIndex}.uploadedDocuments.${fieldName}`
] = (doc as any)._id;
mergedRow.uploadedDocuments[fieldName] = (doc as any)._id;
uploadedItems.push(fieldName);
}
}
if (uploadedItems.length === 0) {
throw new BadRequestException("No files were uploaded");
}
// Check if all requested items are now uploaded
const allItemsCompleted = requestedItems.every((item) =>
uploadedItems.includes(item),
const allItemsCompleted = (requestedItems as string[]).every((item) =>
isResendPartyItemSatisfied(item, mergedRow),
);
if (allItemsCompleted) {
@@ -5901,28 +6013,27 @@ export class RequestManagementService {
new Date();
}
// Update the request
await this.blameRequestDbService.findByIdAndUpdate(requestId, updatePayload);
// Check if all parties completed their resend
const updatedRequest = await this.blameRequestDbService.findById(requestId);
const allPartiesCompleted = updatedRequest.expert?.resend?.parties?.every(
(p) => p.completed,
);
if (allPartiesCompleted) {
// All parties completed - move back to WAITING_FOR_EXPERT
await this.blameRequestDbService.findByIdAndUpdate(requestId, {
$set: {
status: CaseStatus.WAITING_FOR_EXPERT,
"workflow.currentStep": "WAITING_FOR_GUILT_DECISION",
"workflow.currentStep": WorkflowStep.WAITING_FOR_GUILT_DECISION,
"workflow.nextStep": null,
},
$push: {
"workflow.completedSteps": "WAITING_FOR_DOCUMENT_RESEND",
"workflow.completedSteps": WorkflowStep.WAITING_FOR_DOCUMENT_RESEND,
},
});
await this.applyLinkedClaimsBlameResendCleared(requestId);
this.logger.log(
`All parties completed resend for request ${requestId}. Status changed to WAITING_FOR_EXPERT`,
);
@@ -6006,20 +6117,51 @@ export class RequestManagementService {
fileUrl: signFile.path,
};
// Update party confirmation
const updatePayload: any = {
$set: {
[`parties.${partyIndex}.confirmation`]: {
partyRole: userParty.role,
accepted: isAccept,
signature: signatureData,
},
},
const confirmationPayload = {
partyRole: userParty.role,
accepted: isAccept,
signature: signatureData,
};
await this.blameRequestDbService.findByIdAndUpdate(requestId, updatePayload);
// Any single rejection stops the case immediately (in-person resolution required).
if (!isAccept) {
await this.blameRequestDbService.findByIdAndUpdate(requestId, {
$set: {
status: CaseStatus.STOPPED,
[`parties.${partyIndex}.confirmation`]: confirmationPayload,
"workflow.nextStep": null,
},
$push: {
history: {
type: "PARTY_REJECTED_EXPERT_DECISION",
timestamp: new Date(),
metadata: {
userId,
partyRole: userParty.role,
},
},
},
});
this.logger.log(
`Party rejected expert decision for request ${requestId}; status STOPPED.`,
);
return {
requestId: String(request._id),
accepted: false,
status: CaseStatus.STOPPED,
message:
"You rejected the expert decision. This case is stopped and requires in-person resolution.",
};
}
await this.blameRequestDbService.findByIdAndUpdate(requestId, {
$set: {
[`parties.${partyIndex}.confirmation`]: confirmationPayload,
},
});
// Check if both parties have signed
const updatedRequest = await this.blameRequestDbService.findById(requestId);
const allPartiesSigned = updatedRequest.parties.every(
(p) => p.confirmation !== undefined && p.confirmation !== null,
@@ -6029,7 +6171,6 @@ export class RequestManagementService {
let message = "Your signature has been recorded successfully";
if (allPartiesSigned) {
// Check if both accepted
const allAccepted = updatedRequest.parties.every(
(p) => p.confirmation?.accepted === true,
);
@@ -6052,19 +6193,21 @@ export class RequestManagementService {
},
});
} else {
// At least one party rejected - needs in-person resolution
finalStatus = CaseStatus.CANCELLED; // or a specific "NEEDS_IN_PERSON" status
finalStatus = CaseStatus.STOPPED;
message =
"One or both parties rejected the decision. Case requires in-person resolution.";
"One party accepted and another rejected the decision. Case is stopped; in-person resolution required.";
await this.blameRequestDbService.findByIdAndUpdate(requestId, {
$set: {
status: CaseStatus.CANCELLED,
"workflow.currentStep": "COMPLETED",
status: CaseStatus.STOPPED,
"workflow.nextStep": null,
},
$push: {
"workflow.completedSteps": "WAITING_FOR_SIGNATURES",
history: {
type: "PARTIES_DISAGREED_ON_EXPERT_DECISION",
timestamp: new Date(),
metadata: {},
},
},
});
}