diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 4bffb97..e636adc 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -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, diff --git a/src/fanavaran/fanavaran-file-role-expert-ids.spec.ts b/src/fanavaran/fanavaran-file-role-expert-ids.spec.ts new file mode 100644 index 0000000..3092f46 --- /dev/null +++ b/src/fanavaran/fanavaran-file-role-expert-ids.spec.ts @@ -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/); + }); +}); diff --git a/src/fanavaran/fanavaran-file-role-expert-ids.ts b/src/fanavaran/fanavaran-file-role-expert-ids.ts new file mode 100644 index 0000000..f085149 --- /dev/null +++ b/src/fanavaran/fanavaran-file-role-expert-ids.ts @@ -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), + ); +} diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 2020b59..79093e2 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -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,