Use FileMaker and FileReviewer Fanavaran expert ids on V4/V5 flows.

Block file create and review when the matching third-party or car-body code is missing, and override GEN.03/GEN.06 ClaimExpertId from those profiles instead of tenant defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 16:26:58 +03:30
parent acb5d2a682
commit ebfb4385de
4 changed files with 270 additions and 9 deletions

View File

@@ -141,6 +141,13 @@ 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 { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
import {
assertFileMakerCanCreateBlameType,
assertFileReviewerCanReviewBlameType,
resolveFileMakerStageOneExpertId,
resolveFileReviewerExpertiseExpertId,
} from "src/fanavaran/fanavaran-file-role-expert-ids";
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
import { FanavaranLocationService } from "src/fanavaran/fanavaran-location.service";
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
@@ -474,6 +481,7 @@ export class ClaimRequestManagementService {
private readonly fanavaranLookupService: FanavaranLookupService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly fileMakerDbService: FileMakerDbService,
private readonly fileReviewerDbService: FileReviewerDbService,
private readonly fieldExpertDbService: FieldExpertDbService,
private readonly plateNormalizer: PlateNormalizerService,
private readonly fanavaranLocationService: FanavaranLocationService,
@@ -5179,6 +5187,55 @@ export class ClaimRequestManagementService {
}
}
/**
* V4/V5 FileMaker flows: GEN.03 ClaimExpertId from file-maker; GEN.06 from file-reviewer.
* Returns null for non–file-maker blames (tenant defaults apply).
*/
private async resolveFanavaranFileRoleExpertIdsForBlame(blame: {
isMadeByFileMaker?: boolean;
type?: BlameRequestType;
initiatedByFieldExpertId?: unknown;
assignedFileReviewerId?: unknown;
}): Promise<{
baseClaimExpertId: number | null;
expertiseClaimExpertId: number | null;
} | null> {
if (!blame?.isMadeByFileMaker || blame.type == null) {
return null;
}
let baseClaimExpertId: number | null = null;
let expertiseClaimExpertId: number | null = null;
if (blame.initiatedByFieldExpertId) {
const fileMaker = await this.fileMakerDbService.findById(
String(blame.initiatedByFieldExpertId),
);
if (fileMaker) {
assertFileMakerCanCreateBlameType(fileMaker, blame.type);
baseClaimExpertId = resolveFileMakerStageOneExpertId(
fileMaker,
blame.type,
);
}
}
if (blame.assignedFileReviewerId) {
const fileReviewer = await this.fileReviewerDbService.findById(
String(blame.assignedFileReviewerId),
);
if (fileReviewer) {
assertFileReviewerCanReviewBlameType(fileReviewer, blame.type);
expertiseClaimExpertId = resolveFileReviewerExpertiseExpertId(
fileReviewer,
blame.type,
);
}
}
return { baseClaimExpertId, expertiseClaimExpertId };
}
private async getPolicyIdFromNationalCode(
nationalCodeOfInsurer: string,
config: {
@@ -6075,6 +6132,19 @@ export class ClaimRequestManagementService {
Object.assign(payload, hull);
}
const fileRoleExpertIds =
await this.resolveFanavaranFileRoleExpertIdsForBlame(blameCase);
if (fileRoleExpertIds) {
if (fileRoleExpertIds.baseClaimExpertId == null) {
throw new BadRequestException({
code: "FILE_MAKER_FANAVARAN_EXPERT_CODE_MISSING",
message:
"This FileMaker file cannot be submitted to Fanavaran: stage-1 claim expert id is missing for this product line.",
});
}
payload.ClaimExpertId = fileRoleExpertIds.baseClaimExpertId;
}
if (persistPayload) {
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
$set: {
@@ -7048,10 +7118,35 @@ export class ClaimRequestManagementService {
const profile = getFanavaranClientProfile(input.clientKey);
const active = getActiveV2ExpertReply(input.claimCase as any);
// GEN.08 ClaimExpertId = tenant ExpertiseClaimExpertId (assessor role).
// Tejaratno: 2709. Parsian: 29. Must NOT reuse GEN.03 ClaimExpertId
// (مسئول پرونده مالی — Tejaratno 4543092 / Parsian 154).
const claimExpertId = profile.defaults.ExpertiseClaimExpertId;
const blameForExpertIds = input.claimCase?.blameRequestId
? await this.blameRequestDbService.findById(
String(input.claimCase.blameRequestId),
)
: null;
// GEN.06 ClaimExpertId = tenant ExpertiseClaimExpertId (assessor role), unless
// V4/V5 FileMaker flow → file-reviewer expertise code for this product line.
let claimExpertId = profile.defaults.ExpertiseClaimExpertId;
if (blameForExpertIds) {
const roleExpertIds =
await this.resolveFanavaranFileRoleExpertIdsForBlame(
blameForExpertIds as {
isMadeByFileMaker?: boolean;
type?: BlameRequestType;
initiatedByFieldExpertId?: unknown;
assignedFileReviewerId?: unknown;
},
);
if (roleExpertIds?.expertiseClaimExpertId != null) {
claimExpertId = roleExpertIds.expertiseClaimExpertId;
} else if (roleExpertIds) {
throw new BadRequestException({
code: "FILE_REVIEWER_FANAVARAN_EXPERT_CODE_MISSING",
message:
"Fanavaran expertise cannot be submitted: assign a FileReviewer with an expertise claim expert id for this product line.",
});
}
}
const warnings: string[] = [];
if (!active?.parts?.length) {
@@ -7106,11 +7201,7 @@ export class ClaimRequestManagementService {
);
if (product === "car-body") {
const blame = input.claimCase?.blameRequestId
? await this.blameRequestDbService.findById(
String(input.claimCase.blameRequestId),
)
: null;
const blame = blameForExpertIds;
return this.buildCarBodyFanavaranExpertisePayload({
clientKey: input.clientKey,
claimCase: input.claimCase,

View File

@@ -0,0 +1,57 @@
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import {
assertFileMakerCanCreateBlameType,
assertFileReviewerCanReviewBlameType,
resolveFileMakerStageOneExpertId,
resolveFileReviewerExpertiseExpertId,
} from "./fanavaran-file-role-expert-ids";
describe("fanavaran-file-role-expert-ids", () => {
it("resolves stage-1 and expertise ids by blame type", () => {
expect(
resolveFileMakerStageOneExpertId(
{
ThirdPartyClaimExpertId: "154",
CarBodyClaimExpertId: "29",
},
BlameRequestType.THIRD_PARTY,
),
).toBe(154);
expect(
resolveFileMakerStageOneExpertId(
{
ThirdPartyClaimExpertId: "154",
CarBodyClaimExpertId: "29",
},
BlameRequestType.CAR_BODY,
),
).toBe(29);
expect(
resolveFileReviewerExpertiseExpertId(
{
ThirdPartyExpertiseClaim: "2709",
CarBodyExpertiseClaim: "15",
},
BlameRequestType.CAR_BODY,
),
).toBe(15);
});
it("blocks file maker create when the product code is missing", () => {
expect(() =>
assertFileMakerCanCreateBlameType(
{ ThirdPartyClaimExpertId: "154" },
BlameRequestType.CAR_BODY,
),
).toThrow(/stage-1 claim expert id/);
});
it("blocks file reviewer when the product code is missing", () => {
expect(() =>
assertFileReviewerCanReviewBlameType(
{ CarBodyExpertiseClaim: "15" },
BlameRequestType.THIRD_PARTY,
),
).toThrow(/expertise claim expert id/);
});
});

View File

@@ -0,0 +1,90 @@
import { BadRequestException } from "@nestjs/common";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { parseFanavaranId } from "src/lookups/fanavaran-last-car-policy";
export type FileMakerFanavaranExpertCodes = {
ThirdPartyClaimExpertId?: string;
CarBodyClaimExpertId?: string;
};
export type FileReviewerFanavaranExpertCodes = {
ThirdPartyExpertiseClaim?: string;
CarBodyExpertiseClaim?: string;
};
export function parseFanavaranExpertId(raw?: string | null): number | null {
if (raw == null) return null;
const trimmed = String(raw).trim();
if (!trimmed) return null;
return parseFanavaranId(trimmed) ?? parseFanavaranId(Number(trimmed));
}
export function fileMakerStageOneExpertCode(
fileMaker: FileMakerFanavaranExpertCodes,
blameType: BlameRequestType,
): string | undefined {
return blameType === BlameRequestType.CAR_BODY
? fileMaker.CarBodyClaimExpertId
: fileMaker.ThirdPartyClaimExpertId;
}
export function fileReviewerExpertiseExpertCode(
fileReviewer: FileReviewerFanavaranExpertCodes,
blameType: BlameRequestType,
): string | undefined {
return blameType === BlameRequestType.CAR_BODY
? fileReviewer.CarBodyExpertiseClaim
: fileReviewer.ThirdPartyExpertiseClaim;
}
function blameTypeLabel(blameType: BlameRequestType): string {
return blameType === BlameRequestType.CAR_BODY
? "car body (بدنه)"
: "third party (ثالث)";
}
export function assertFileMakerCanCreateBlameType(
fileMaker: FileMakerFanavaranExpertCodes,
blameType: BlameRequestType,
): void {
const raw = fileMakerStageOneExpertCode(fileMaker, blameType);
if (parseFanavaranExpertId(raw) != null) {
return;
}
throw new BadRequestException({
code: "FILE_MAKER_FANAVARAN_EXPERT_CODE_MISSING",
message: `Your FileMaker profile has no Fanavaran stage-1 claim expert id for ${blameTypeLabel(blameType)} files. You cannot create this file type.`,
});
}
export function assertFileReviewerCanReviewBlameType(
fileReviewer: FileReviewerFanavaranExpertCodes,
blameType: BlameRequestType,
): void {
const raw = fileReviewerExpertiseExpertCode(fileReviewer, blameType);
if (parseFanavaranExpertId(raw) != null) {
return;
}
throw new BadRequestException({
code: "FILE_REVIEWER_FANAVARAN_EXPERT_CODE_MISSING",
message: `Your FileReviewer profile has no Fanavaran expertise claim expert id for ${blameTypeLabel(blameType)} files. You cannot review this file type.`,
});
}
export function resolveFileMakerStageOneExpertId(
fileMaker: FileMakerFanavaranExpertCodes,
blameType: BlameRequestType,
): number | null {
return parseFanavaranExpertId(
fileMakerStageOneExpertCode(fileMaker, blameType),
);
}
export function resolveFileReviewerExpertiseExpertId(
fileReviewer: FileReviewerFanavaranExpertCodes,
blameType: BlameRequestType,
): number | null {
return parseFanavaranExpertId(
fileReviewerExpertiseExpertCode(fileReviewer, blameType),
);
}

View File

@@ -52,6 +52,12 @@ import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
import { UserDbService } from "src/users/entities/db-service/user.db.service";
import { FanavaranLocationService } from "src/fanavaran/fanavaran-location.service";
import {
assertFileMakerCanCreateBlameType,
assertFileReviewerCanReviewBlameType,
} from "src/fanavaran/fanavaran-file-role-expert-ids";
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
import { isOtpExpiryActive } from "src/helpers/user-otp-expiry";
import { parseIranLocalDateTime } from "src/helpers/iran-datetime";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
@@ -1088,6 +1094,8 @@ export class RequestManagementService {
private readonly hashService: HashService,
private readonly userAuthService: UserAuthService,
private readonly fanavaranLocationService: FanavaranLocationService,
private readonly fileMakerDbService: FileMakerDbService,
private readonly fileReviewerDbService: FileReviewerDbService,
) {}
/**
@@ -5325,6 +5333,14 @@ export class RequestManagementService {
"This file has been taken by another FileReviewer.",
);
}
const fileReviewer = await this.fileReviewerDbService.findById(
String(expert.sub),
);
if (!fileReviewer) {
throw new ForbiddenException("FileReviewer account not found.");
}
assertFileReviewerCanReviewBlameType(fileReviewer, req.type);
if (!assignedId) {
// Atomically claim — ignore if another reviewer won the race (they would have
// been caught by the assignedId check above on their own first call).
@@ -5633,6 +5649,13 @@ export class RequestManagementService {
}
const isFileMakerRole = (expert as any)?.role === RoleEnum.FILE_MAKER;
if (isFileMakerRole) {
const fileMaker = await this.fileMakerDbService.findById(String(expert.sub));
if (!fileMaker) {
throw new ForbiddenException("FileMaker account not found.");
}
assertFileMakerCanCreateBlameType(fileMaker, type);
}
const created = await this.blameRequestDbService.create({
publicId,
requestNo: publicId,