forked from Yara724/api
YARA-789
This commit is contained in:
@@ -63,7 +63,9 @@ import { PublicIdService } from "src/utils/public-id/public-id.service";
|
||||
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
|
||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
|
||||
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
|
||||
import { CreationMethod } from "src/request-management/entities/schema/request-management.schema";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||
|
||||
@Injectable()
|
||||
@@ -336,6 +338,166 @@ export class ClaimRequestManagementService {
|
||||
return trueItems;
|
||||
}
|
||||
|
||||
/** One section of CarDamagePartDto: either a single object or an array of objects (legacy). */
|
||||
private carPartDamageSectionToRows(section: unknown): Record<string, unknown>[] {
|
||||
if (section == null) return [];
|
||||
if (Array.isArray(section)) {
|
||||
return section.filter(
|
||||
(row): row is Record<string, unknown> =>
|
||||
!!row && typeof row === "object",
|
||||
);
|
||||
}
|
||||
if (typeof section === "object") {
|
||||
return [section as Record<string, unknown>];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private carPartDamageSectionHasTrue(section: unknown): boolean {
|
||||
for (const row of this.carPartDamageSectionToRows(section)) {
|
||||
for (const val of Object.values(row)) {
|
||||
if (val === true) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* At most two of left, right, front, back may contain any damaged part (true).
|
||||
* `top` and claim `otherParts` are not subject to this rule.
|
||||
*/
|
||||
private assertCarPartDamageAtMostTwoOfFourSides(dto: CarDamagePartDto): void {
|
||||
const d = dto as unknown as Record<string, unknown>;
|
||||
const quadrants = [
|
||||
{ key: "left", section: d.left },
|
||||
{ key: "right", section: d.right },
|
||||
{ key: "front", section: d.front },
|
||||
{ key: "back", section: d.back },
|
||||
];
|
||||
const used = quadrants.filter((q) =>
|
||||
this.carPartDamageSectionHasTrue(q.section),
|
||||
);
|
||||
if (used.length > 2) {
|
||||
throw new BadRequestException(
|
||||
`At most two of the four sides (left, right, front, back) may include damaged parts. ` +
|
||||
`Sides with selections: ${used.map((u) => u.key).join(", ")}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps legacy expert CarDamagePartDto (nested booleans) to v2 outer part slugs (ClaimCase.damage.selectedParts).
|
||||
*/
|
||||
private carDamagePartDtoToOuterPartSlugs(dto: CarDamagePartDto): string[] {
|
||||
const out = new Set<string>();
|
||||
const add = (slug: string | undefined) => {
|
||||
if (slug) out.add(slug);
|
||||
};
|
||||
const walkMainOnSide = (
|
||||
side: "left" | "right",
|
||||
items: unknown,
|
||||
map: Record<string, string>,
|
||||
) => {
|
||||
for (const row of this.carPartDamageSectionToRows(items)) {
|
||||
for (const [key, val] of Object.entries(row)) {
|
||||
if (val !== true) continue;
|
||||
const slug = map[`${side}_${key}`] ?? map[key];
|
||||
add(slug);
|
||||
}
|
||||
}
|
||||
};
|
||||
// MainParts on left / right (side-specific door / fender)
|
||||
walkMainOnSide("left", (dto as any).left, {
|
||||
left_frontDoor: "front_left_door",
|
||||
left_backDoor: "rear_left_door",
|
||||
left_frontFender: "front_left_fender",
|
||||
left_backFender: "rear_left_fender",
|
||||
frontDoor: "front_left_door",
|
||||
backDoor: "rear_left_door",
|
||||
frontFender: "front_left_fender",
|
||||
backFender: "rear_left_fender",
|
||||
});
|
||||
walkMainOnSide("right", (dto as any).right, {
|
||||
right_frontDoor: "front_right_door",
|
||||
right_backDoor: "rear_right_door",
|
||||
right_frontFender: "front_right_fender",
|
||||
right_backFender: "rear_right_fender",
|
||||
frontDoor: "front_right_door",
|
||||
backDoor: "rear_right_door",
|
||||
frontFender: "front_right_fender",
|
||||
backFender: "rear_right_fender",
|
||||
});
|
||||
const walkSimple = (items: unknown, keyToSlug: Record<string, string>) => {
|
||||
for (const row of this.carPartDamageSectionToRows(items)) {
|
||||
for (const [key, val] of Object.entries(row)) {
|
||||
if (val === true) add(keyToSlug[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
walkSimple((dto as any).front, {
|
||||
frontBumper: "front_bumper",
|
||||
carHood: "hood",
|
||||
});
|
||||
walkSimple((dto as any).back, {
|
||||
backBumper: "rear_bumper",
|
||||
carTrunk: "trunk",
|
||||
});
|
||||
walkSimple((dto as any).top, {
|
||||
roof: "roof",
|
||||
});
|
||||
return Array.from(out);
|
||||
}
|
||||
|
||||
/** Normalize expert DTO otherParts (strings or legacy { label: true }[]) to string[]. */
|
||||
private normalizeExpertOtherPartsInput(raw: any[] | undefined): string[] {
|
||||
if (!raw || !Array.isArray(raw)) return [];
|
||||
const out: string[] = [];
|
||||
for (const item of raw) {
|
||||
if (typeof item === "string" && item.trim()) {
|
||||
out.push(item.trim());
|
||||
continue;
|
||||
}
|
||||
if (item && typeof item === "object") {
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
if (v === true && k) out.push(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private async assertFieldExpertInPersonBlameForClaimCase(
|
||||
claimCase: any,
|
||||
expert: any,
|
||||
): Promise<void> {
|
||||
const blameRequestId = claimCase?.blameRequestId?.toString?.();
|
||||
if (!blameRequestId) {
|
||||
throw new BadRequestException("Claim case has no linked blame request.");
|
||||
}
|
||||
const blameRequest = await this.blameRequestDbService.findById(blameRequestId);
|
||||
if (!blameRequest) {
|
||||
throw new NotFoundException("Blame request not found for this claim.");
|
||||
}
|
||||
if (
|
||||
!blameRequest.expertInitiated ||
|
||||
blameRequest.creationMethod !== CreationMethod.IN_PERSON
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON claim files.",
|
||||
);
|
||||
}
|
||||
if (!blameRequest.initiatedByFieldExpertId) {
|
||||
throw new BadRequestException(
|
||||
"Blame request is missing initiating field expert.",
|
||||
);
|
||||
}
|
||||
if (String(blameRequest.initiatedByFieldExpertId) !== String(expert.sub)) {
|
||||
throw new ForbiddenException(
|
||||
"Only the initiating field expert can complete claim data for this file.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async setCarPartDamageAndImage(cl, trueItems) {
|
||||
cl.carPartDamage = trueItems;
|
||||
const jsonTrueItems = JSON.parse(JSON.stringify(trueItems));
|
||||
@@ -389,7 +551,9 @@ export class ClaimRequestManagementService {
|
||||
|
||||
/**
|
||||
* Field expert completes claim-needed data for expert-initiated IN_PERSON files.
|
||||
* Only the initiating expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts.
|
||||
* Only the initiating field expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts.
|
||||
*
|
||||
* V2: Resolves `ClaimCase` in `claimCases` collection (workflow). Legacy v1 claim documents are still supported.
|
||||
*/
|
||||
async expertCompleteClaimData(
|
||||
claimRequestId: string,
|
||||
@@ -401,6 +565,16 @@ export class ClaimRequestManagementService {
|
||||
otherParts?: any[];
|
||||
},
|
||||
): Promise<{ success: boolean; claimRequestId: string }> {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (claimCase) {
|
||||
return this.expertCompleteClaimDataForClaimCaseV2(
|
||||
claimRequestId,
|
||||
expert,
|
||||
dto,
|
||||
claimCase,
|
||||
);
|
||||
}
|
||||
|
||||
const claim = await this.claimDbService.findOne(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim file not found");
|
||||
@@ -422,6 +596,7 @@ export class ClaimRequestManagementService {
|
||||
if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) {
|
||||
throw new BadRequestException("Car part damage is already set.");
|
||||
}
|
||||
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage);
|
||||
const trueItems = this.findTrueItems(dto.carPartDamage);
|
||||
const claimDoc = (await this.claimDbService.findOne(
|
||||
claimRequestId,
|
||||
@@ -450,6 +625,178 @@ export class ClaimRequestManagementService {
|
||||
return { success: true, claimRequestId };
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 ClaimCase: outer parts + optional bank/other parts (expert-initiated IN_PERSON only).
|
||||
*/
|
||||
private async expertCompleteClaimDataForClaimCaseV2(
|
||||
claimRequestId: string,
|
||||
expert: any,
|
||||
dto: {
|
||||
carPartDamage?: CarDamagePartDto;
|
||||
sheba?: string;
|
||||
nationalCodeOfInsurer?: string;
|
||||
otherParts?: any[];
|
||||
},
|
||||
claimCase: any,
|
||||
): Promise<{ success: boolean; claimRequestId: string }> {
|
||||
await this.assertFieldExpertInPersonBlameForClaimCase(claimCase, expert);
|
||||
|
||||
let working = claimCase;
|
||||
|
||||
if (dto.carPartDamage) {
|
||||
if (working.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OUTER_PARTS) {
|
||||
throw new BadRequestException(
|
||||
`Cannot set outer parts: workflow step must be ${ClaimWorkflowStep.SELECT_OUTER_PARTS}. Current: ${working.workflow?.currentStep}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
working.damage?.selectedParts &&
|
||||
working.damage.selectedParts.length > 0
|
||||
) {
|
||||
throw new BadRequestException("Car part damage is already set.");
|
||||
}
|
||||
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage);
|
||||
const selectedParts = this.carDamagePartDtoToOuterPartSlugs(dto.carPartDamage);
|
||||
if (!selectedParts.length) {
|
||||
throw new BadRequestException(
|
||||
"No outer damaged parts could be derived from carPartDamage. Check part selections.",
|
||||
);
|
||||
}
|
||||
|
||||
const updated = await this.claimCaseDbService.findByIdAndUpdate(
|
||||
claimRequestId,
|
||||
{
|
||||
"damage.selectedParts": selectedParts,
|
||||
status: ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||
"workflow.currentStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
"workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
$push: {
|
||||
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
history: {
|
||||
type: "STEP_COMPLETED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName:
|
||||
`${expert.firstName || ""} ${expert.lastName || ""}`.trim() ||
|
||||
"field_expert",
|
||||
actorType: "field_expert",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
selectedParts,
|
||||
description: "Field expert submitted outer damage parts",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!updated) {
|
||||
throw new InternalServerErrorException("Failed to update claim case");
|
||||
}
|
||||
working = updated;
|
||||
}
|
||||
|
||||
const hasBankOrOther =
|
||||
dto.sheba != null ||
|
||||
dto.nationalCodeOfInsurer != null ||
|
||||
dto.otherParts != null;
|
||||
if (hasBankOrOther) {
|
||||
const fresh = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!fresh) throw new NotFoundException("Claim case not found");
|
||||
if (fresh.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OTHER_PARTS) {
|
||||
throw new BadRequestException(
|
||||
`Bank/other parts can only be set at ${ClaimWorkflowStep.SELECT_OTHER_PARTS}. Current: ${fresh.workflow?.currentStep}`,
|
||||
);
|
||||
}
|
||||
|
||||
const mergedSheba =
|
||||
dto.sheba != null ? dto.sheba : fresh.money?.sheba;
|
||||
const mergedNational =
|
||||
dto.nationalCodeOfInsurer != null
|
||||
? dto.nationalCodeOfInsurer
|
||||
: fresh.money?.nationalCodeOfInsurer;
|
||||
|
||||
if (fresh.money?.sheba && dto.sheba != null && dto.sheba !== fresh.money.sheba) {
|
||||
throw new ConflictException("Sheba is already set for this claim.");
|
||||
}
|
||||
if (
|
||||
fresh.money?.nationalCodeOfInsurer &&
|
||||
dto.nationalCodeOfInsurer != null &&
|
||||
dto.nationalCodeOfInsurer !== fresh.money.nationalCodeOfInsurer
|
||||
) {
|
||||
throw new ConflictException(
|
||||
"National code of insurer is already set for this claim.",
|
||||
);
|
||||
}
|
||||
|
||||
const otherPartsArr = this.normalizeExpertOtherPartsInput(dto.otherParts);
|
||||
const setPatch: Record<string, unknown> = {};
|
||||
if (dto.sheba != null) setPatch["money.sheba"] = dto.sheba;
|
||||
if (dto.nationalCodeOfInsurer != null) {
|
||||
setPatch["money.nationalCodeOfInsurer"] = dto.nationalCodeOfInsurer;
|
||||
}
|
||||
if (dto.otherParts != null) {
|
||||
setPatch["damage.otherParts"] =
|
||||
otherPartsArr.length > 0 ? otherPartsArr : [];
|
||||
}
|
||||
|
||||
const bankComplete =
|
||||
mergedSheba &&
|
||||
mergedNational &&
|
||||
/^[0-9]{24}$/.test(String(mergedSheba).replace(/\s/g, "")) &&
|
||||
/^[0-9]{10}$/.test(String(mergedNational).replace(/\s/g, ""));
|
||||
|
||||
if (bankComplete) {
|
||||
const shebaNorm = String(mergedSheba).replace(/\s/g, "");
|
||||
const nationalNorm = String(mergedNational).replace(/\s/g, "");
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
"money.sheba": shebaNorm,
|
||||
"money.nationalCodeOfInsurer": nationalNorm,
|
||||
...(dto.otherParts != null
|
||||
? {
|
||||
"damage.otherParts":
|
||||
otherPartsArr.length > 0 ? otherPartsArr : [],
|
||||
}
|
||||
: {}),
|
||||
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||
"workflow.currentStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
"workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
$push: {
|
||||
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
history: {
|
||||
type: "STEP_COMPLETED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName:
|
||||
`${expert.firstName || ""} ${expert.lastName || ""}`.trim() ||
|
||||
"field_expert",
|
||||
actorType: "field_expert",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
description:
|
||||
"Field expert submitted bank info and optional other parts",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (Object.keys(setPatch).length === 0) {
|
||||
throw new BadRequestException(
|
||||
"Provide sheba (24 digits) and nationalCodeOfInsurer (10 digits) together to finish this step, or supply partial fields incrementally.",
|
||||
);
|
||||
}
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: setPatch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, claimRequestId };
|
||||
}
|
||||
|
||||
async selectCarOtherPartDamage(
|
||||
requestId: string,
|
||||
body: any, // Use your DTO for better type safety
|
||||
|
||||
Reference in New Issue
Block a user