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 { ImageRequiredModel } from "./entites/schema/image-required.schema";
|
||||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
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 { 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";
|
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -336,6 +338,166 @@ export class ClaimRequestManagementService {
|
|||||||
return trueItems;
|
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) {
|
async setCarPartDamageAndImage(cl, trueItems) {
|
||||||
cl.carPartDamage = trueItems;
|
cl.carPartDamage = trueItems;
|
||||||
const jsonTrueItems = JSON.parse(JSON.stringify(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.
|
* 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(
|
async expertCompleteClaimData(
|
||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
@@ -401,6 +565,16 @@ export class ClaimRequestManagementService {
|
|||||||
otherParts?: any[];
|
otherParts?: any[];
|
||||||
},
|
},
|
||||||
): Promise<{ success: boolean; claimRequestId: string }> {
|
): 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);
|
const claim = await this.claimDbService.findOne(claimRequestId);
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
throw new NotFoundException("Claim file not found");
|
throw new NotFoundException("Claim file not found");
|
||||||
@@ -422,6 +596,7 @@ export class ClaimRequestManagementService {
|
|||||||
if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) {
|
if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) {
|
||||||
throw new BadRequestException("Car part damage is already set.");
|
throw new BadRequestException("Car part damage is already set.");
|
||||||
}
|
}
|
||||||
|
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage);
|
||||||
const trueItems = this.findTrueItems(dto.carPartDamage);
|
const trueItems = this.findTrueItems(dto.carPartDamage);
|
||||||
const claimDoc = (await this.claimDbService.findOne(
|
const claimDoc = (await this.claimDbService.findOne(
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
@@ -450,6 +625,178 @@ export class ClaimRequestManagementService {
|
|||||||
return { success: true, claimRequestId };
|
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(
|
async selectCarOtherPartDamage(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
body: any, // Use your DTO for better type safety
|
body: any, // Use your DTO for better type safety
|
||||||
|
|||||||
Reference in New Issue
Block a user