From 0bb13f459620333a91685e1a538542800f55c6b7 Mon Sep 17 00:00:00 2001 From: "s.yahyaee" Date: Fri, 1 May 2026 16:01:08 +0330 Subject: [PATCH] Fix workflow steps --- .../resend-item-ui.ts | 53 +++++++++++++- src/expert-blame/expert-blame.service.ts | 22 ++++-- src/expert-claim/expert-claim.service.ts | 6 +- .../request-management.service.ts | 70 ++++++++++--------- .../request-management.v2.controller.ts | 13 +++- 5 files changed, 123 insertions(+), 41 deletions(-) diff --git a/src/Types&Enums/blame-request-management/resend-item-ui.ts b/src/Types&Enums/blame-request-management/resend-item-ui.ts index 687da2a..0417452 100644 --- a/src/Types&Enums/blame-request-management/resend-item-ui.ts +++ b/src/Types&Enums/blame-request-management/resend-item-ui.ts @@ -1,5 +1,55 @@ import { ResendItemType } from "./resendItemType.enum"; +const RESEND_ITEM_VALUES = new Set(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(); + 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 { + if (raw == null || typeof raw !== "object") return {}; + if (raw instanceof Map) { + const o: Record = {}; + for (const [k, v] of raw.entries()) { + o[String(k)] = v; + } + return o; + } + return { ...(raw as Record) }; +} + /** How the mobile/web client should collect each resend item (no workflow-step manager). */ export type ResendItemInputKind = "document_camera" | "voice" | "video" | "text"; @@ -11,7 +61,8 @@ export function getResendItemInputKind(item: string): ResendItemInputKind { } export function buildResendItemsWithUi(requestedItems: string[]) { - return requestedItems.map((item) => ({ + const normalized = normalizeResendRequestedItemsList(requestedItems); + return normalized.map((item) => ({ item, inputKind: getResendItemInputKind(item), })); diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index 2afe394..11e24d5 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -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 { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.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 { toJalaliDateAndTime } from "src/helpers/date-jalali"; import { ResendRequestDto } from "./dto/resend.dto"; @@ -1148,7 +1151,15 @@ export class ExpertBlameService { `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))) { throw new BadRequestException( `Invalid requestedItems value: ${item}. Use ResendItemType values.`, @@ -1162,9 +1173,12 @@ export class ExpertBlameService { const partyResendRequests = resendDto.parties.map((party) => { const uid = String(party.partyId); + const normalizedItems = normalizeResendRequestedItemsList( + party.requestedItems as string[], + ); return { partyId: new Types.ObjectId(uid), - requestedItems: party.requestedItems, + requestedItems: normalizedItems, description: party.description || "", requestedAt: now, completed: false, @@ -1228,7 +1242,7 @@ export class ExpertBlameService { } } const partyHasResendPayload = (row: { - requestedItems?: ResendItemType[]; + requestedItems?: readonly string[]; description?: string; }) => { const n = Array.isArray(row.requestedItems) diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index f960a0f..d1e39d7 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -2546,8 +2546,8 @@ export class ExpertClaimService { }; const nextStep = needsFactorUpload - ? ClaimWorkflowStep.EXPERT_COST_EVALUATION - : ClaimWorkflowStep.INSURER_REVIEW; + ? ClaimWorkflowStep.INSURER_REVIEW + : ClaimWorkflowStep.CLAIM_COMPLETED; const nextCaseStatus = ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL; const nextClaimStatus = needsFactorUpload @@ -2566,7 +2566,7 @@ export class ExpertClaimService { }, 'workflow.currentStep': nextStep, 'workflow.nextStep': needsFactorUpload - ? ClaimWorkflowStep.INSURER_REVIEW + ? ClaimWorkflowStep.EXPERT_COST_EVALUATION : ClaimWorkflowStep.CLAIM_COMPLETED, [`evaluation.${replyField}`]: replyPayload, $push: { diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 581349f..30b1046 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -28,7 +28,10 @@ import { UserSignatureResponseDto } from "./dto/user-signature.dto"; import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum"; import { buildResendItemsWithUi, + cloneResendUploadedDocuments, isResendPartyItemSatisfied, + normalizeResendRequestedItemKey, + normalizeResendRequestedItemsList, } 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"; @@ -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 merged = { - uploadedDocuments: { - ...(typeof row.uploadedDocuments === "object" && row.uploadedDocuments - ? { ...row.uploadedDocuments } - : {}), - }, + uploadedDocuments: cloneResendUploadedDocuments(row.uploadedDocuments), resendVoiceId: row.resendVoiceId, resendVideoId: row.resendVideoId, userTextDescription: row.userTextDescription, @@ -6056,7 +6057,7 @@ export class RequestManagementService { return { requestId: String(request._id), - requestedItems: userResendRequest.requestedItems, + requestedItems: items, description: userResendRequest.description || "", completed: userResendRequest.completed || false, requestedItemsUi: buildResendItemsWithUi(items), @@ -6122,22 +6123,26 @@ export class RequestManagementService { } 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 mergedRow = { - uploadedDocuments: { - ...(typeof row.uploadedDocuments === "object" && row.uploadedDocuments - ? { ...row.uploadedDocuments } - : {}), - }, + uploadedDocuments: cloneResendUploadedDocuments(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]; + const safeFiles = files || {}; + const hasAnyFile = Object.keys(safeFiles).some((fieldName) => { + const fileArray = safeFiles[fieldName as keyof typeof safeFiles]; return fileArray && fileArray.length > 0 && fileArray[0]; }); const descTrim = @@ -6147,7 +6152,7 @@ export class RequestManagementService { if (!hasAnyFile && !sendingDescription) { 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); } - for (const fieldName in files) { - const fileArray = files[fieldName as keyof typeof files]; + for (const fieldName of Object.keys(safeFiles)) { + const fileArray = safeFiles[fieldName as keyof typeof safeFiles]; if (!fileArray || fileArray.length === 0) continue; const file = fileArray[0]; - if (!requestedItems.includes(fieldName)) { + const canonKey = normalizeResendRequestedItemKey(fieldName); + if (!canonKey || !requestedItems.includes(canonKey)) { 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({ path: file.path, requestId: new Types.ObjectId(requestId), @@ -6188,8 +6194,8 @@ export class RequestManagementService { `expert.resend.parties.${resendIndex}.resendVoiceId` ] = (voiceDoc as any)._id; mergedRow.resendVoiceId = (voiceDoc as any)._id; - uploadedItems.push(fieldName); - } else if (fieldName === "video") { + uploadedItems.push(canonKey); + } else if (canonKey === ResendItemType.VIDEO) { const videoDoc = await this.blameVideoDbService.create({ path: file.path, requestId: new Types.ObjectId(requestId), @@ -6201,9 +6207,9 @@ export class RequestManagementService { `expert.resend.parties.${resendIndex}.resendVideoId` ] = (videoDoc as any)._id; mergedRow.resendVideoId = (videoDoc as any)._id; - uploadedItems.push(fieldName); + uploadedItems.push(canonKey); } else { - const docType = fieldName as BlameDocumentType; + const docType = canonKey as BlameDocumentType; const doc = await this.blameDocumentDbService.create({ path: file.path, requestId: new Types.ObjectId(requestId), @@ -6211,16 +6217,16 @@ export class RequestManagementService { documentType: docType, }); updatePayload.$set[ - `expert.resend.parties.${resendIndex}.uploadedDocuments.${fieldName}` + `expert.resend.parties.${resendIndex}.uploadedDocuments.${canonKey}` ] = (doc as any)._id; - mergedRow.uploadedDocuments[fieldName] = (doc as any)._id; - uploadedItems.push(fieldName); + mergedRow.uploadedDocuments[canonKey] = (doc as any)._id; + uploadedItems.push(canonKey); } } - const allItemsCompleted = (requestedItems as string[]).every((item) => - isResendPartyItemSatisfied(item, mergedRow), - ); + const allItemsCompleted = + requestedItems.length > 0 && + requestedItems.every((item) => isResendPartyItemSatisfied(item, mergedRow)); if (allItemsCompleted) { updatePayload.$set[`expert.resend.parties.${resendIndex}.completed`] = true; @@ -6231,7 +6237,7 @@ export class RequestManagementService { await this.blameRequestDbService.findByIdAndUpdate(requestId, updatePayload); const updatedRequest = await this.blameRequestDbService.findById(requestId); - const allPartiesCompleted = updatedRequest.expert?.resend?.parties?.every( + const allPartiesCompleted = updatedRequest?.expert?.resend?.parties?.every( (p) => p.completed, ); diff --git a/src/request-management/request-management.v2.controller.ts b/src/request-management/request-management.v2.controller.ts index ed8b239..920ccac 100644 --- a/src/request-management/request-management.v2.controller.ts +++ b/src/request-management/request-management.v2.controller.ts @@ -399,6 +399,11 @@ export class RequestManagementV2Controller { 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[]; }, @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( requestId, user.sub, files, - description, + mergedDescription, ); } }