forked from Yara724/api
update some important issues
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
|
||||
export class UserResendResponseDto {
|
||||
@ApiProperty({
|
||||
@@ -24,6 +24,22 @@ export class UserResendResponseDto {
|
||||
description: "Whether the user has completed uploading all requested items",
|
||||
})
|
||||
completed: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Per-item UI hint: document_camera (open camera), voice, video, or text (multiline field).",
|
||||
example: [
|
||||
{ item: "drivingLicense", inputKind: "document_camera" },
|
||||
{ item: "description", inputKind: "text" },
|
||||
],
|
||||
})
|
||||
requestedItemsUi?: { item: string; inputKind: string }[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Requested items this party has not yet satisfied (partial resend).",
|
||||
example: ["carCertificate"],
|
||||
})
|
||||
remainingItems?: string[];
|
||||
}
|
||||
|
||||
export class UserResendUploadResponseDto {
|
||||
|
||||
@@ -64,6 +64,10 @@ export class PartyResendRequest {
|
||||
@Prop({ type: [String], default: [] })
|
||||
requestedItems: string[]; // Array of ResendItemType values
|
||||
|
||||
/** @deprecated Resend UI uses requestedItems + inputKind from API; do not rely on workflow steps. */
|
||||
@Prop({ type: [String], default: [] })
|
||||
workflowStepKeys?: string[];
|
||||
|
||||
@Prop()
|
||||
description?: string;
|
||||
|
||||
@@ -75,6 +79,20 @@ export class PartyResendRequest {
|
||||
|
||||
@Prop({ type: Date })
|
||||
completedAt?: Date;
|
||||
|
||||
/** Document resend uploads keyed by ResendItemType (e.g. drivingLicense). */
|
||||
@Prop({ type: Object, default: {} })
|
||||
uploadedDocuments?: Record<string, Types.ObjectId>;
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
resendVoiceId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
resendVideoId?: Types.ObjectId;
|
||||
|
||||
/** Fulfills requestedItems `description` (text). */
|
||||
@Prop({ type: String })
|
||||
userTextDescription?: string;
|
||||
}
|
||||
export const PartyResendRequestSchema = SchemaFactory.createForClass(PartyResendRequest);
|
||||
|
||||
@@ -89,6 +107,12 @@ export class ExpertResend {
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
requestedByExpertId?: Types.ObjectId;
|
||||
|
||||
/**
|
||||
* @deprecated Unused for resend UI; clients use per-party requestedItems + inputKind.
|
||||
*/
|
||||
@Prop({ type: [String], default: [] })
|
||||
combinedResubmitStepKeys?: string[];
|
||||
}
|
||||
export const ExpertResendSchema = SchemaFactory.createForClass(ExpertResend);
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -304,8 +311,67 @@ export class RequestManagementService {
|
||||
private readonly workflowStepDbService: WorkflowStepDbService,
|
||||
private readonly hashService: HashService,
|
||||
private readonly userAuthService: UserAuthService,
|
||||
private readonly claimCaseDbService: ClaimCaseDbService,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
@@ -5673,11 +5739,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5695,6 +5777,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) {
|
||||
@@ -5718,7 +5801,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");
|
||||
}
|
||||
@@ -5738,50 +5823,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,
|
||||
@@ -5789,22 +5909,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) {
|
||||
@@ -5813,28 +5927,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`,
|
||||
);
|
||||
@@ -5918,20 +6031,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,
|
||||
@@ -5941,7 +6085,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,
|
||||
);
|
||||
@@ -5964,19 +6107,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: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -387,6 +387,11 @@ export class RequestManagementV2Controller {
|
||||
format: "binary",
|
||||
description: "Video file (if requested)",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
description:
|
||||
"Text description when expert requested ResendItemType.description",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -427,11 +432,13 @@ export class RequestManagementV2Controller {
|
||||
voice?: Express.Multer.File[];
|
||||
video?: Express.Multer.File[];
|
||||
},
|
||||
@Body("description") description?: string,
|
||||
) {
|
||||
return this.requestManagementService.uploadResendDocumentsV2(
|
||||
requestId,
|
||||
user.sub,
|
||||
files,
|
||||
description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user