1
0
forked from Yara724/api

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";
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). */
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),
}));

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 { 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)

View File

@@ -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: {

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 {
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,
);

View File

@@ -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,
);
}
}