Fix workflow steps

This commit is contained in:
2026-05-01 16:01:08 +03:30
parent d9dc4ecdff
commit 0bb13f4596
5 changed files with 123 additions and 41 deletions

View File

@@ -1,5 +1,55 @@
import { ResendItemType } from "./resendItemType.enum"; import { ResendItemType } from "./resendItemType.enum";
const RESEND_ITEM_VALUES = new Set<string>(Object.values(ResendItemType));
/**
* Map multipart / client field names and DB typos to canonical {@link ResendItemType} values.
*/
export function normalizeResendRequestedItemKey(raw: string): string | null {
const t = String(raw ?? "").trim();
if (!t) return null;
if (RESEND_ITEM_VALUES.has(t)) return t;
const lower = t.toLowerCase();
for (const v of RESEND_ITEM_VALUES) {
if (v.toLowerCase() === lower) return v;
}
return null;
}
/** Deduplicated list of valid requested item keys. */
export function normalizeResendRequestedItemsList(
items: string[] | undefined | null,
): string[] {
const out: string[] = [];
const seen = new Set<string>();
for (const raw of items || []) {
const c = normalizeResendRequestedItemKey(String(raw));
if (c && !seen.has(c)) {
seen.add(c);
out.push(c);
}
}
return out;
}
/**
* Clone `uploadedDocuments` from a Mongoose subdoc (plain object or Map) into a plain object
* so merges and {@link isResendPartyItemSatisfied} see existing keys.
*/
export function cloneResendUploadedDocuments(
raw: unknown,
): Record<string, unknown> {
if (raw == null || typeof raw !== "object") return {};
if (raw instanceof Map) {
const o: Record<string, unknown> = {};
for (const [k, v] of raw.entries()) {
o[String(k)] = v;
}
return o;
}
return { ...(raw as Record<string, unknown>) };
}
/** How the mobile/web client should collect each resend item (no workflow-step manager). */ /** How the mobile/web client should collect each resend item (no workflow-step manager). */
export type ResendItemInputKind = "document_camera" | "voice" | "video" | "text"; export type ResendItemInputKind = "document_camera" | "voice" | "video" | "text";
@@ -11,7 +61,8 @@ export function getResendItemInputKind(item: string): ResendItemInputKind {
} }
export function buildResendItemsWithUi(requestedItems: string[]) { export function buildResendItemsWithUi(requestedItems: string[]) {
return requestedItems.map((item) => ({ const normalized = normalizeResendRequestedItemsList(requestedItems);
return normalized.map((item) => ({
item, item,
inputKind: getResendItemInputKind(item), inputKind: getResendItemInputKind(item),
})); }));

View File

@@ -34,7 +34,10 @@ import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatu
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum"; import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum"; import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum"; import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum";
import { buildResendItemsWithUi } from "src/Types&Enums/blame-request-management/resend-item-ui"; import {
buildResendItemsWithUi,
normalizeResendRequestedItemsList,
} from "src/Types&Enums/blame-request-management/resend-item-ui";
import { buildFileLink } from "src/helpers/urlCreator"; import { buildFileLink } from "src/helpers/urlCreator";
import { toJalaliDateAndTime } from "src/helpers/date-jalali"; import { toJalaliDateAndTime } from "src/helpers/date-jalali";
import { ResendRequestDto } from "./dto/resend.dto"; import { ResendRequestDto } from "./dto/resend.dto";
@@ -1148,7 +1151,15 @@ export class ExpertBlameService {
`partyId ${uid} is not a party userId on this blame request.`, `partyId ${uid} is not a party userId on this blame request.`,
); );
} }
for (const item of party.requestedItems || []) { const normalizedItems = normalizeResendRequestedItemsList(
party.requestedItems as string[],
);
if (normalizedItems.length === 0) {
throw new BadRequestException(
`Party ${uid}: requestedItems must include at least one valid resend item (e.g. drivingLicense, voice, description).`,
);
}
for (const item of normalizedItems) {
if (!allowedItems.has(String(item))) { if (!allowedItems.has(String(item))) {
throw new BadRequestException( throw new BadRequestException(
`Invalid requestedItems value: ${item}. Use ResendItemType values.`, `Invalid requestedItems value: ${item}. Use ResendItemType values.`,
@@ -1162,9 +1173,12 @@ export class ExpertBlameService {
const partyResendRequests = resendDto.parties.map((party) => { const partyResendRequests = resendDto.parties.map((party) => {
const uid = String(party.partyId); const uid = String(party.partyId);
const normalizedItems = normalizeResendRequestedItemsList(
party.requestedItems as string[],
);
return { return {
partyId: new Types.ObjectId(uid), partyId: new Types.ObjectId(uid),
requestedItems: party.requestedItems, requestedItems: normalizedItems,
description: party.description || "", description: party.description || "",
requestedAt: now, requestedAt: now,
completed: false, completed: false,
@@ -1228,7 +1242,7 @@ export class ExpertBlameService {
} }
} }
const partyHasResendPayload = (row: { const partyHasResendPayload = (row: {
requestedItems?: ResendItemType[]; requestedItems?: readonly string[];
description?: string; description?: string;
}) => { }) => {
const n = Array.isArray(row.requestedItems) const n = Array.isArray(row.requestedItems)

View File

@@ -2546,8 +2546,8 @@ export class ExpertClaimService {
}; };
const nextStep = needsFactorUpload const nextStep = needsFactorUpload
? ClaimWorkflowStep.EXPERT_COST_EVALUATION ? ClaimWorkflowStep.INSURER_REVIEW
: ClaimWorkflowStep.INSURER_REVIEW; : ClaimWorkflowStep.CLAIM_COMPLETED;
const nextCaseStatus = ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL; const nextCaseStatus = ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL;
const nextClaimStatus = needsFactorUpload const nextClaimStatus = needsFactorUpload
@@ -2566,7 +2566,7 @@ export class ExpertClaimService {
}, },
'workflow.currentStep': nextStep, 'workflow.currentStep': nextStep,
'workflow.nextStep': needsFactorUpload 'workflow.nextStep': needsFactorUpload
? ClaimWorkflowStep.INSURER_REVIEW ? ClaimWorkflowStep.EXPERT_COST_EVALUATION
: ClaimWorkflowStep.CLAIM_COMPLETED, : ClaimWorkflowStep.CLAIM_COMPLETED,
[`evaluation.${replyField}`]: replyPayload, [`evaluation.${replyField}`]: replyPayload,
$push: { $push: {

View File

@@ -28,7 +28,10 @@ import { UserSignatureResponseDto } from "./dto/user-signature.dto";
import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum"; import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum";
import { import {
buildResendItemsWithUi, buildResendItemsWithUi,
cloneResendUploadedDocuments,
isResendPartyItemSatisfied, isResendPartyItemSatisfied,
normalizeResendRequestedItemKey,
normalizeResendRequestedItemsList,
} from "src/Types&Enums/blame-request-management/resend-item-ui"; } 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 { 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 { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
@@ -6040,14 +6043,12 @@ export class RequestManagementService {
); );
} }
const items = (userResendRequest.requestedItems || []) as string[]; const items = normalizeResendRequestedItemsList(
userResendRequest.requestedItems as string[],
);
const row = userResendRequest as any; const row = userResendRequest as any;
const merged = { const merged = {
uploadedDocuments: { uploadedDocuments: cloneResendUploadedDocuments(row.uploadedDocuments),
...(typeof row.uploadedDocuments === "object" && row.uploadedDocuments
? { ...row.uploadedDocuments }
: {}),
},
resendVoiceId: row.resendVoiceId, resendVoiceId: row.resendVoiceId,
resendVideoId: row.resendVideoId, resendVideoId: row.resendVideoId,
userTextDescription: row.userTextDescription, userTextDescription: row.userTextDescription,
@@ -6056,7 +6057,7 @@ export class RequestManagementService {
return { return {
requestId: String(request._id), requestId: String(request._id),
requestedItems: userResendRequest.requestedItems, requestedItems: items,
description: userResendRequest.description || "", description: userResendRequest.description || "",
completed: userResendRequest.completed || false, completed: userResendRequest.completed || false,
requestedItemsUi: buildResendItemsWithUi(items), requestedItemsUi: buildResendItemsWithUi(items),
@@ -6122,22 +6123,26 @@ export class RequestManagementService {
} }
const userResendRequest = resendParties[resendIndex]; const userResendRequest = resendParties[resendIndex];
const requestedItems = userResendRequest.requestedItems || []; const requestedItems = normalizeResendRequestedItemsList(
userResendRequest.requestedItems as string[],
);
if (requestedItems.length === 0) {
throw new BadRequestException(
"No valid requested items are configured for your party resend. Please contact support.",
);
}
const row = userResendRequest as any; const row = userResendRequest as any;
const mergedRow = { const mergedRow = {
uploadedDocuments: { uploadedDocuments: cloneResendUploadedDocuments(row.uploadedDocuments),
...(typeof row.uploadedDocuments === "object" && row.uploadedDocuments
? { ...row.uploadedDocuments }
: {}),
},
resendVoiceId: row.resendVoiceId, resendVoiceId: row.resendVoiceId,
resendVideoId: row.resendVideoId, resendVideoId: row.resendVideoId,
userTextDescription: row.userTextDescription, userTextDescription: row.userTextDescription,
}; };
const hasAnyFile = Object.keys(files || {}).some((fieldName) => { const safeFiles = files || {};
const fileArray = files[fieldName as keyof typeof files]; const hasAnyFile = Object.keys(safeFiles).some((fieldName) => {
const fileArray = safeFiles[fieldName as keyof typeof safeFiles];
return fileArray && fileArray.length > 0 && fileArray[0]; return fileArray && fileArray.length > 0 && fileArray[0];
}); });
const descTrim = const descTrim =
@@ -6147,7 +6152,7 @@ export class RequestManagementService {
if (!hasAnyFile && !sendingDescription) { if (!hasAnyFile && !sendingDescription) {
throw new BadRequestException( throw new BadRequestException(
"Provide at least one requested file or a text description when description was requested.", "Provide at least one file for a requested item and/or the text description when the expert asked for a description.",
); );
} }
@@ -6162,19 +6167,20 @@ export class RequestManagementService {
uploadedItems.push(ResendItemType.DESCRIPTION); uploadedItems.push(ResendItemType.DESCRIPTION);
} }
for (const fieldName in files) { for (const fieldName of Object.keys(safeFiles)) {
const fileArray = files[fieldName as keyof typeof files]; const fileArray = safeFiles[fieldName as keyof typeof safeFiles];
if (!fileArray || fileArray.length === 0) continue; if (!fileArray || fileArray.length === 0) continue;
const file = fileArray[0]; const file = fileArray[0];
if (!requestedItems.includes(fieldName)) { const canonKey = normalizeResendRequestedItemKey(fieldName);
if (!canonKey || !requestedItems.includes(canonKey)) {
throw new BadRequestException( throw new BadRequestException(
`${fieldName} was not requested. Requested items: ${requestedItems.join(", ")}`, `${fieldName} was not requested or is not a valid resend field. Requested items: ${requestedItems.join(", ")}`,
); );
} }
if (fieldName === "voice") { if (canonKey === ResendItemType.VOICE) {
const voiceDoc = await this.blameVoiceDbService.create({ const voiceDoc = await this.blameVoiceDbService.create({
path: file.path, path: file.path,
requestId: new Types.ObjectId(requestId), requestId: new Types.ObjectId(requestId),
@@ -6188,8 +6194,8 @@ export class RequestManagementService {
`expert.resend.parties.${resendIndex}.resendVoiceId` `expert.resend.parties.${resendIndex}.resendVoiceId`
] = (voiceDoc as any)._id; ] = (voiceDoc as any)._id;
mergedRow.resendVoiceId = (voiceDoc as any)._id; mergedRow.resendVoiceId = (voiceDoc as any)._id;
uploadedItems.push(fieldName); uploadedItems.push(canonKey);
} else if (fieldName === "video") { } else if (canonKey === ResendItemType.VIDEO) {
const videoDoc = await this.blameVideoDbService.create({ const videoDoc = await this.blameVideoDbService.create({
path: file.path, path: file.path,
requestId: new Types.ObjectId(requestId), requestId: new Types.ObjectId(requestId),
@@ -6201,9 +6207,9 @@ export class RequestManagementService {
`expert.resend.parties.${resendIndex}.resendVideoId` `expert.resend.parties.${resendIndex}.resendVideoId`
] = (videoDoc as any)._id; ] = (videoDoc as any)._id;
mergedRow.resendVideoId = (videoDoc as any)._id; mergedRow.resendVideoId = (videoDoc as any)._id;
uploadedItems.push(fieldName); uploadedItems.push(canonKey);
} else { } else {
const docType = fieldName as BlameDocumentType; const docType = canonKey as BlameDocumentType;
const doc = await this.blameDocumentDbService.create({ const doc = await this.blameDocumentDbService.create({
path: file.path, path: file.path,
requestId: new Types.ObjectId(requestId), requestId: new Types.ObjectId(requestId),
@@ -6211,16 +6217,16 @@ export class RequestManagementService {
documentType: docType, documentType: docType,
}); });
updatePayload.$set[ updatePayload.$set[
`expert.resend.parties.${resendIndex}.uploadedDocuments.${fieldName}` `expert.resend.parties.${resendIndex}.uploadedDocuments.${canonKey}`
] = (doc as any)._id; ] = (doc as any)._id;
mergedRow.uploadedDocuments[fieldName] = (doc as any)._id; mergedRow.uploadedDocuments[canonKey] = (doc as any)._id;
uploadedItems.push(fieldName); uploadedItems.push(canonKey);
} }
} }
const allItemsCompleted = (requestedItems as string[]).every((item) => const allItemsCompleted =
isResendPartyItemSatisfied(item, mergedRow), requestedItems.length > 0 &&
); requestedItems.every((item) => isResendPartyItemSatisfied(item, mergedRow));
if (allItemsCompleted) { if (allItemsCompleted) {
updatePayload.$set[`expert.resend.parties.${resendIndex}.completed`] = true; updatePayload.$set[`expert.resend.parties.${resendIndex}.completed`] = true;
@@ -6231,7 +6237,7 @@ export class RequestManagementService {
await this.blameRequestDbService.findByIdAndUpdate(requestId, updatePayload); await this.blameRequestDbService.findByIdAndUpdate(requestId, updatePayload);
const updatedRequest = await this.blameRequestDbService.findById(requestId); const updatedRequest = await this.blameRequestDbService.findById(requestId);
const allPartiesCompleted = updatedRequest.expert?.resend?.parties?.every( const allPartiesCompleted = updatedRequest?.expert?.resend?.parties?.every(
(p) => p.completed, (p) => p.completed,
); );

View File

@@ -399,6 +399,11 @@ export class RequestManagementV2Controller {
description: description:
"Text description when expert requested ResendItemType.description", "Text description when expert requested ResendItemType.description",
}, },
textDescription: {
type: "string",
description:
"Alias for `description` (same semantics); either field may be used.",
},
}, },
}, },
}) })
@@ -442,12 +447,18 @@ export class RequestManagementV2Controller {
video?: Express.Multer.File[]; video?: Express.Multer.File[];
}, },
@Body("description") description?: string, @Body("description") description?: string,
@Body("textDescription") textDescription?: string,
) { ) {
const descCandidates = [description, textDescription]
.map((x) => (x != null ? String(x).trim() : ""))
.filter((s) => s.length > 0);
const mergedDescription =
descCandidates.length > 0 ? descCandidates[0] : undefined;
return this.requestManagementService.uploadResendDocumentsV2( return this.requestManagementService.uploadResendDocumentsV2(
requestId, requestId,
user.sub, user.sub,
files, files,
description, mergedDescription,
); );
} }
} }