From acb5d2a6827c6c3e2e0afd0bdfecc4334d90ace5 Mon Sep 17 00:00:00 2001 From: "s.hajizadeh" Date: Wed, 16 Sep 2026 16:05:54 +0330 Subject: [PATCH 1/2] car body implemented --- .../claim-request-management.service.ts | 482 ++++++++++++++++-- .../fanavaran-hull-base-claim.spec.ts | 79 +++ .../fanavaran-hull-base-claim.ts | 140 +++++ .../fanavaran-hull-expertise-lookups.ts | 76 +++ .../fanavaran-hull-expertise.spec.ts | 22 +- .../fanavaran-hull-expertise.ts | 56 +- src/core/config/fanavaran-client.config.ts | 2 + src/fanavaran/fanavaran-lookup.config.ts | 20 + src/fanavaran/fanavaran-lookup.service.ts | 10 + src/lookups/lookups.controller.ts | 79 +++ src/lookups/lookups.service.ts | 30 ++ 11 files changed, 957 insertions(+), 39 deletions(-) create mode 100644 src/claim-request-management/fanavaran-hull-base-claim.spec.ts create mode 100644 src/claim-request-management/fanavaran-hull-base-claim.ts create mode 100644 src/claim-request-management/fanavaran-hull-expertise-lookups.ts diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index b1e9e59..4bffb97 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -49,13 +49,31 @@ import { pickStoredCarBodyPolicyId, type FanavaranClaimProduct, } from "./fanavaran-claim-product"; +import { + actualPremiumFromHullPolicyRecord, + customerIdFromHullPolicyRecord, + FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID, + fanavaranHullNestedClaimId, + toFanavaranHullBaseClaimPayload, +} from "./fanavaran-hull-base-claim"; import { collectFanavaranExpertiseReadinessWarnings, hullExpertiseAssertFields, + mergeHullVehicleIdentityFromFanavaranVehicle, pickHullVehicleIdentity, toFanavaranHullExpertiseDmgSection, toFanavaranHullExpertisePayload, + vehicleCurrentValueFromHullPolicyRecord, } from "./fanavaran-hull-expertise"; +import { + asHullDmgAccessoryRows, + FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID, + FANAVARAN_DEFAULT_HULL_DMG_KIND_ID, + FANAVARAN_HULL_DMG_COST_KIND_CAPTION, + FANAVARAN_HULL_DMG_KIND_CAPTION, + findLookupIdByCaption, + resolveVehicleHullAccessoryId, +} from "./fanavaran-hull-expertise-lookups"; import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto"; import { @@ -4219,6 +4237,307 @@ export class ClaimRequestManagementService { } } + /** GEN.03 hull: TotalPremium + parties inquiry (AccidentCulpritId) before hull field whitelist. */ + private async enrichCarBodyHullBaseClaimPayload(input: { + clientKey: FanavaranClientKey; + payload: Record; + blameCase: { parties?: unknown[] }; + /** Blame `Party` row (FIRST party for CAR_BODY); person shape is read via pickPerson* helpers. */ + firstParty?: { person?: unknown }; + logPrefix: string; + }): Promise { + const policyId = parseFanavaranId(input.payload.PolicyId); + if (policyId == null) return; + + let policyRecord: Record | null = null; + try { + policyRecord = asObjectRecord( + await this.fanavaranLookupService.bodyPolicyById( + input.clientKey, + policyId, + { + contractIdOverride: resolveFanavaranProductContractId( + input.clientKey, + "car-body", + ), + }, + ), + ); + } catch (error) { + this.logger.warn( + `${input.logPrefix} hull base claim body policy GET ${policyId} failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } + + const actualPremium = actualPremiumFromHullPolicyRecord(policyRecord); + if (actualPremium != null) { + input.payload.ActualPremium = actualPremium; + } + + const person = input.firstParty?.person as + | { + driverIsInsurer?: boolean; + nationalCodeOfDriver?: unknown; + nationalCodeOfInsurer?: unknown; + nationalCode?: unknown; + birthday?: unknown; + driverBirthday?: unknown; + insurerBirthday?: unknown; + } + | undefined; + + const nationalCode = + pickPersonNationalCode(person) ?? + pickCarBodyPolicyNationalCode( + input.blameCase as { parties?: unknown[] }, + ); + const birthday = pickPersonBirthday(person); + const driverIsInsurer = person?.driverIsInsurer ?? true; + + let accidentCulpritId: number | null = null; + if (nationalCode && birthday) { + accidentCulpritId = await this.resolveDriverFanavaranId( + input.clientKey, + nationalCode, + birthday, + driverIsInsurer, + ); + } + if (accidentCulpritId == null) { + accidentCulpritId = customerIdFromHullPolicyRecord(policyRecord); + if (accidentCulpritId != null) { + this.logger.log( + `${input.logPrefix} AccidentCulpritId from policy CustomerId=${accidentCulpritId}`, + ); + } + } + if (accidentCulpritId != null) { + input.payload.AccidentCulpritId = accidentCulpritId; + } + + const culpritTypeId = + parseFanavaranId(input.payload.CulpritTypeId) ?? + FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID; + if ( + input.payload.CustomerFaultPercent == null && + culpritTypeId === 300 + ) { + input.payload.CustomerFaultPercent = 100; + } + } + + /** GEN.06: VehicleCurrentValue from hull policy when price-drop / frontend value is missing. */ + private async resolveHullVehicleCurrentValueFromPolicy( + clientKey: FanavaranClientKey, + claimCase: { + fanavaranSync?: { + baseClaim?: { policyId?: unknown; lastPayload?: { PolicyId?: unknown } }; + }; + }, + ): Promise { + const policyId = parseFanavaranId( + claimCase?.fanavaranSync?.baseClaim?.policyId ?? + claimCase?.fanavaranSync?.baseClaim?.lastPayload?.PolicyId, + ); + if (policyId == null) return null; + try { + const policy = asObjectRecord( + await this.fanavaranLookupService.bodyPolicyById( + clientKey, + policyId, + { + contractIdOverride: resolveFanavaranProductContractId( + clientKey, + "car-body", + ), + }, + ), + ); + return vehicleCurrentValueFromHullPolicyRecord(policy); + } catch (error) { + this.logger.warn( + `[resolveHullVehicleCurrentValueFromPolicy] body policy GET ${policyId} failed: ${ + error instanceof Error ? error.message : error + }`, + ); + return null; + } + } + + private parseHullClaimPolicyId(claimCase: { + fanavaranSync?: { + baseClaim?: { policyId?: unknown; lastPayload?: { PolicyId?: unknown } }; + }; + }): number | null { + return parseFanavaranId( + claimCase?.fanavaranSync?.baseClaim?.policyId ?? + claimCase?.fanavaranSync?.baseClaim?.lastPayload?.PolicyId, + ); + } + + private async buildCarBodyFanavaranExpertisePayload(input: { + clientKey: FanavaranClientKey; + claimCase: any; + blame: { parties?: unknown[] } | null; + profile: ReturnType; + parts: any[]; + claimExpertId: number; + submittedAt: Date; + priceDropTotal: number; + priceDropCarPrice: unknown; + warnings: string[]; + replyKey?: string; + }): Promise<{ + payload: Record; + warnings: string[]; + replyKey?: string; + }> { + const policyId = this.parseHullClaimPolicyId(input.claimCase); + const contractOptions = { + contractIdOverride: resolveFanavaranProductContractId( + input.clientKey, + "car-body", + ), + }; + + const [dmgKindRows, dmgCostKindRows, policyRecord, accessoriesRaw] = + await Promise.all([ + this.getFanavaranLookupRows(input.clientKey, "vehicle-hull-dmg-kind"), + this.getFanavaranLookupRows( + input.clientKey, + "vehicle-hull-dmg-cost-kinds", + ), + policyId != null + ? this.fanavaranLookupService.bodyPolicyById( + input.clientKey, + policyId, + contractOptions, + ) + : Promise.resolve(null), + policyId != null + ? this.fanavaranLookupService.vehicleHullDmgAccessoriesByPolicyId( + input.clientKey, + policyId, + contractOptions, + ) + : Promise.resolve([]), + ]); + + const dmgKindId = + findLookupIdByCaption(dmgKindRows, FANAVARAN_HULL_DMG_KIND_CAPTION) ?? + FANAVARAN_DEFAULT_HULL_DMG_KIND_ID; + const dmgCostKindId = + findLookupIdByCaption( + dmgCostKindRows, + FANAVARAN_HULL_DMG_COST_KIND_CAPTION, + ) ?? FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID; + + const accessories = asHullDmgAccessoryRows(accessoriesRaw); + if (policyId != null && accessories.length === 0) { + input.warnings.push( + `No hull dmg-accessories for policy ${policyId}; VehicleHullAccessoryId may be missing.`, + ); + } + + let vehicle = pickHullVehicleIdentity(input.blame); + let colorId: number | null = null; + const policy = asObjectRecord(policyRecord); + const vehicleId = pickVehicleId(policy?.VehicleId ?? policy?.vehicleId); + const versionNo = parseFanavaranId(policy?.VehicleVersionNo); + if (vehicleId != null) { + try { + const vehicleRow = asObjectRecord( + await this.fanavaranLookupService.vehicleById( + input.clientKey, + vehicleId, + versionNo ?? undefined, + contractOptions, + ), + ); + vehicle = mergeHullVehicleIdentityFromFanavaranVehicle( + vehicle, + vehicleRow, + ); + colorId = parseFanavaranId(vehicleRow.ColorId); + } catch (error) { + this.logger.warn( + `[buildCarBodyFanavaranExpertisePayload] vehicle GET ${vehicleId} failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } + } + + const wage = input.parts.reduce( + (sum, part) => sum + this.parseFanavaranMoney(part?.salary), + 0, + ); + const componentReplacementCost = input.parts.reduce( + (sum, part) => sum + this.parseFanavaranMoney(part?.price), + 0, + ); + const wasteValue = input.parts.reduce( + (sum, part) => sum + this.fanavaranDaghiWasteValue(part?.daghi), + 0, + ); + + const hullSections = input.parts.map((part: any) => { + const accessoryKindId = parseCatalogPartIdInput(part?.partId); + const vehicleHullAccessoryId = resolveVehicleHullAccessoryId( + accessories, + accessoryKindId, + ); + if (vehicleHullAccessoryId == null) { + input.warnings.push( + `No VehicleHullAccessoryId for part "${this.fanavaranPartLabel(part)}" (AccessoryKindId=${accessoryKindId ?? "none"}).`, + ); + } + return toFanavaranHullExpertiseDmgSection({ + partId: part?.partId, + desc: this.fanavaranPartLabel(part), + wasteValue: this.fanavaranDaghiWasteValue(part?.daghi), + amount: + this.parseFanavaranMoney(part?.price) + + this.parseFanavaranMoney(part?.salary), + dmgKindId, + dmgCostKindId, + vehicleHullAccessoryId, + }); + }); + + let vehicleCurrentValue = + this.parseFanavaranMoney(input.priceDropCarPrice) || null; + if (vehicleCurrentValue == null) { + vehicleCurrentValue = vehicleCurrentValueFromHullPolicyRecord(policy); + } + + const repairDuration = + typeof input.profile.defaults.HullExpertiseRepairDuration === "number" + ? input.profile.defaults.HullExpertiseRepairDuration + : null; + + return { + replyKey: input.replyKey, + warnings: input.warnings, + payload: toFanavaranHullExpertisePayload({ + claimExpertId: input.claimExpertId, + dmgAssessmentDate: this.convertToPersianDate(input.submittedAt), + inspectionTime: this.getTime24Hour(input.submittedAt), + wage, + componentReplacementCost, + wasteValue, + dropAmount: input.priceDropTotal || 0, + vehicleCurrentValue, + vehicle, + colorId, + repairDuration, + dmgSections: hullSections, + }), + }; + } + private async buildFanavaranDamageCasePayload(input: { claimCase: any; blameCase?: any; @@ -5743,6 +6062,19 @@ export class ClaimRequestManagementService { ); } + if (fanavaranClaimProductFromBlameType(blameCase.type) === "car-body") { + await this.enrichCarBodyHullBaseClaimPayload({ + clientKey, + payload, + blameCase, + firstParty, + logPrefix, + }); + const hull = toFanavaranHullBaseClaimPayload(payload); + for (const key of Object.keys(payload)) delete payload[key]; + Object.assign(payload, hull); + } + if (persistPayload) { await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { $set: { @@ -6153,7 +6485,34 @@ export class ClaimRequestManagementService { }; } - const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/files`; + await this.syncFanavaranHullBaseClaim(claimCaseId, clientKey); + claimCase = await this.claimCaseDbService.findById(claimCaseId); + const nestedClaimId = fanavaranHullNestedClaimId({ + claimId: claimCase?.claimId, + claimNo: claimCase?.claimNo, + }); + if (nestedClaimId == null) { + return { + clientKey, + claimCaseId, + totalLocalImages: candidates.length, + skippedAlreadySubmitted: candidates.length - pending.length, + attempted: 0, + submitted: 0, + failed: 0, + skipped: pending.length, + warning: "Fanavaran base claimId is missing", + results: pending.map((c) => ({ + attempted: false, + submitted: false, + skipped: true, + skipReason: "Fanavaran base claimId is missing", + fileName: c.fileName, + })), + }; + } + + const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${nestedClaimId}/files`; const fileTypeId = profile.defaults.ClaimFileTypeId; const product = await this.resolveFanavaranClaimProduct(claimCaseId); @@ -6166,7 +6525,7 @@ export class ClaimRequestManagementService { const content = { FileName: candidate.fileName, FileTypeId: fileTypeId, - ...(product === "car-body" ? { ClaimId: claimCase.claimId } : {}), + ...(product === "car-body" ? { ClaimId: nestedClaimId } : {}), }; this.logger.log( @@ -6188,7 +6547,7 @@ export class ClaimRequestManagementService { results.push({ attempted: true, submitted: true, - claimId: claimCase.claimId, + claimId: nestedClaimId, fileName: candidate.fileName, submitUrl: url, fanavaranResponse: response.data, @@ -6203,7 +6562,7 @@ export class ClaimRequestManagementService { submitted: false, warning, fileName: candidate.fileName, - claimId: claimCase.claimId, + claimId: nestedClaimId, submitUrl: url, }); @@ -6752,33 +7111,19 @@ export class ClaimRequestManagementService { String(input.claimCase.blameRequestId), ) : null; - const hullSections = parts.map((part: any) => - toFanavaranHullExpertiseDmgSection({ - partId: part?.partId, - desc: this.fanavaranPartLabel(part), - wasteValue: this.fanavaranDaghiWasteValue(part?.daghi), - amount: - this.parseFanavaranMoney(part?.price) + - this.parseFanavaranMoney(part?.salary), - }), - ); - return { - replyKey: active?.replyKey, + return this.buildCarBodyFanavaranExpertisePayload({ + clientKey: input.clientKey, + claimCase: input.claimCase, + blame, + profile, + parts, + claimExpertId, + submittedAt, + priceDropTotal, + priceDropCarPrice: priceDrop?.carPrice, warnings, - payload: toFanavaranHullExpertisePayload({ - claimExpertId, - dmgAssessmentDate: this.convertToPersianDate(submittedAt), - inspectionTime: this.getTime24Hour(submittedAt), - wage: repairWage, - componentReplacementCost, - wasteValue, - dropAmount: priceDropTotal || 0, - vehicleCurrentValue: - this.parseFanavaranMoney(priceDrop?.carPrice) || null, - vehicle: pickHullVehicleIdentity(blame), - dmgSections: hullSections, - }), - }; + replyKey: active?.replyKey, + }); } const payload: Record = { @@ -6940,7 +7285,18 @@ export class ClaimRequestManagementService { } ); } - const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/expertise`; + await this.syncFanavaranHullBaseClaim(claimCaseId, clientKey); + claimCase = await this.claimCaseDbService.findById(claimCaseId); + const nestedClaimId = fanavaranHullNestedClaimId({ + claimId: claimCase?.claimId, + claimNo: claimCase?.claimNo, + }); + if (nestedClaimId == null) { + throw new BadRequestException( + "Fanavaran claimId is required before submitting expertise", + ); + } + const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${nestedClaimId}/expertise`; const startedAt = Date.now(); const auditSession: FanavaranAuditSession = { trackingCode: this.fanavaranAuditService.generateTrackingCode(), @@ -7332,6 +7688,70 @@ export class ClaimRequestManagementService { } } + private async putFanavaranJson( + url: string, + payload: Record, + clientKey: FanavaranClientKey, + auditSession?: FanavaranAuditSession, + claimCaseId?: string, + ) { + this.fanavaranAuthService.assertNotInBackoff(clientKey); + const headers = await this.getFanavaranAuthHeaders( + clientKey, + auditSession, + claimCaseId, + ); + + try { + const response = await firstValueFrom( + this.httpService.put(url, payload, { + headers: { + ...headers, + "Content-Type": "application/json", + }, + }), + ); + this.fanavaranAuthService.clearBackoff(clientKey); + return response; + } catch (error) { + this.fanavaranAuthService.registerFailure(clientKey, error); + throw error; + } + } + + /** + * Nested hull files/expertise look up a بدنه پرونده. Create often echoed a ثالث-shaped + * body; PUT the GEN.03 hull shape onto the existing Id before follow-up stages. + */ + private async syncFanavaranHullBaseClaim( + claimCaseId: string, + clientKey: FanavaranClientKey, + ): Promise { + const product = await this.resolveFanavaranClaimProduct(claimCaseId); + if (product !== "car-body") return; + const claimCase = await this.claimCaseDbService.findById(claimCaseId); + const nestedId = fanavaranHullNestedClaimId({ + claimId: claimCase?.claimId, + claimNo: claimCase?.claimNo, + }); + if (nestedId == null) return; + + const built = await this.previewFanavaranSubmitV2(claimCaseId, clientKey, { + requirePolicyId: true, + persistPayload: true, + }); + const hull = toFanavaranHullBaseClaimPayload( + built && typeof built === "object" && "payload" in built + ? (built as { payload: Record }).payload + : built, + ); + const url = `${fanavaranClaimsBaseUrl("car-body")}/${nestedId}`; + this.logger.log( + `[Fanavaran hull sync] PUT ${url} keys=${Object.keys(hull).join(",")}`, + ); + await this.putFanavaranJson(url, hull, clientKey, undefined, claimCaseId); + } + private async postFanavaranMultipart( url: string, content: Record, diff --git a/src/claim-request-management/fanavaran-hull-base-claim.spec.ts b/src/claim-request-management/fanavaran-hull-base-claim.spec.ts new file mode 100644 index 0000000..7be8206 --- /dev/null +++ b/src/claim-request-management/fanavaran-hull-base-claim.spec.ts @@ -0,0 +1,79 @@ +import { + actualPremiumFromHullPolicyRecord, + customerIdFromHullPolicyRecord, + FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID, + fanavaranHullNestedClaimId, + toFanavaranHullBaseClaimPayload, +} from "./fanavaran-hull-base-claim"; + +describe("fanavaran hull base claim", () => { + it("reads ActualPremium and CustomerId from hull policy GET", () => { + expect( + actualPremiumFromHullPolicyRecord({ TotalPremium: 21613552 }), + ).toBe(21613552); + expect(customerIdFromHullPolicyRecord({ CustomerId: 1230091 })).toBe( + 1230091, + ); + }); + + it("defaults CustomerFaultPercent to 100 when CulpritTypeId is 300", () => { + const hull = toFanavaranHullBaseClaimPayload({ + PolicyId: 1, + CulpritTypeId: 300, + }); + expect(hull.CustomerFaultPercent).toBe(100); + }); + + it("uses GEN.03 Id for nested files/expertise, not ClaimNo", () => { + expect( + fanavaranHullNestedClaimId({ Id: 5023617, ClaimNo: 2268 }), + ).toBe(5023617); + expect( + fanavaranHullNestedClaimId({ claimId: 5023617, claimNo: 2268 }), + ).toBe(5023617); + expect(fanavaranHullNestedClaimId({ ClaimNo: 2268 })).toBeNull(); + }); + + it("strips ثالث-only fields from a live hull create echo", () => { + const hull = toFanavaranHullBaseClaimPayload({ + AccidentCauseId: 6, + AccidentCityId: 701, + AccidentDate: "1405/06/24", + AccidentLocationAddress: "استان تهران شهر تهران", + AccidentReportTypeId: 155, + AccidentTime: "10:33", + AccidentVehicleUsedId: 1, + ActualPremium: 57270462, + AnnouncementDate: "1405/06/24", + ClaimExpertId: 154, + ClaimNo: 2268, + CompensationReferenceId: 167, + CulpritLicenceNo: "9705463515", + CulpritTypeId: 337, + CustomerFaultPercent: 100, + DamagedCount: 1, + EstimateAmount: 16000000, + HasOtherCulprit: 0, + Id: 5023617, + IsFatalAccident: 0, + IsLicenseReplacement: null, + IsPlaqueChanged: 0, + PolicyId: 13764408, + PreviousPolicyEndDate: "1404/10/23", + SanhabVersion: 6, + }); + + expect(hull.PolicyId).toBe(13764408); + expect(hull.AccidentTypeId).toBe(FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID); + expect(hull.IsLicenseReplaced).toBeNull(); + expect(hull.CostSeparationToDmgSections).toBe(0); + expect(hull.IsOwnerChanged).toBe(0); + expect(hull.CulpritTypeId).toBe(301); + expect(hull.DamagedCount).toBeUndefined(); + expect(hull.HasOtherCulprit).toBeUndefined(); + expect(hull.CompensationReferenceId).toBeUndefined(); + expect(hull.SanhabVersion).toBeUndefined(); + expect(hull.Id).toBeUndefined(); + expect(hull.ClaimNo).toBeUndefined(); + }); +}); diff --git a/src/claim-request-management/fanavaran-hull-base-claim.ts b/src/claim-request-management/fanavaran-hull-base-claim.ts new file mode 100644 index 0000000..8ef1c93 --- /dev/null +++ b/src/claim-request-management/fanavaran-hull-base-claim.ts @@ -0,0 +1,140 @@ +/** GEN.03 VHUD input fields. Do not send ثالث-only keys (DamagedCount, HasOtherCulprit, plaques, …). */ +export const FANAVARAN_HULL_BASE_CLAIM_KEYS = [ + "ArchiveNo", + "AccidentDate", + "AccidentTime", + "AnnouncementDate", + "AccidentLocationAddress", + "ClaimExpertId", + "EntryDate", + "CulpritLicenceNo", + "CulpritLicenceIssuDate", + "CulpritLicenceForeignCityName", + "PoliceReportSeri", + "PoliceReportSerial", + "PoliceReportDesc", + "CustomerFaultPercent", + "CulpritLevelTwoLicenceIssuDate", + "ActualPremium", + "EstimateAmount", + "TrackingCode", + "CourtArchiveNo", + "PlaqueReplacementDate", + "StatusChangeDate", + "ClaimCompletionDate", + "DmgAssessorFirstCreationTime", + "PolicyId", + "IsSurplusArticleEighthLaw", + "AccidentCityId", + "AccidentCauseId", + "AccidentTypeId", + "GlassBreakReasonId", + "CulpritTypeId", + "AuthorityCulpritId", + "AccidentCulpritId", + "CulpritLicenceTypeId", + "IsLicenseReplaced", + "CulpritLicenceCountryId", + "CulpritLicenceCityId", + "AccidentReportTypeId", + "PoliceOfficerId", + "IsAccidentOutOfBorder", + "AccidentVehicleUsedId", + "IsOwnerChanged", + "CostSeparationToDmgSections", +] as const; + +/** GEN.03 default: تصادف(حادثه). Lookup: GET /lookups/vehicle-hull-accident-types */ +export const FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID = 2; + +/** GEN.03 ans (0=خیر). Used for CostSeparationToDmgSections, IsOwnerChanged, IsLicenseReplaced. */ +export const FANAVARAN_HULL_ANS_NO = 0; + +/** GEN.03 hull culprit type when third-party is at fault. Lookup: vehicle-hull-accident-culprit-type */ +export const FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID = 301; + +/** GEN.03 ActualPremium from GET car/vehicle-hull-policies/{PolicyId} → TotalPremium. */ +export function actualPremiumFromHullPolicyRecord( + policy: Record | null | undefined, +): number | null { + if (!policy) return null; + const raw = policy.TotalPremium ?? policy.totalPremium; + const n = typeof raw === "number" ? raw : Number(raw); + return Number.isFinite(n) && n >= 0 ? n : null; +} + +/** Fallback AccidentCulpritId when parties inquiry is unavailable. */ +export function customerIdFromHullPolicyRecord( + policy: Record | null | undefined, +): number | null { + if (!policy) return null; + const raw = policy.CustomerId ?? policy.customerId; + const n = typeof raw === "number" ? raw : Number(raw); + return Number.isFinite(n) && n > 0 ? n : null; +} + +export function toFanavaranHullBaseClaimPayload( + built: Record, +): Record { + const next: Record = {}; + for (const key of FANAVARAN_HULL_BASE_CLAIM_KEYS) { + if (key === "IsLicenseReplaced") { + next[key] = + built.IsLicenseReplaced ?? built.IsLicenseReplacement ?? null; + continue; + } + if (key === "AccidentTypeId") { + next[key] = + built.AccidentTypeId ?? FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID; + continue; + } + if (key === "CostSeparationToDmgSections") { + next[key] = + built.CostSeparationToDmgSections ?? FANAVARAN_HULL_ANS_NO; + continue; + } + if (key === "IsOwnerChanged") { + next[key] = built.IsOwnerChanged ?? FANAVARAN_HULL_ANS_NO; + continue; + } + if (key === "CulpritTypeId") { + const raw = built.CulpritTypeId; + // ثالث profile default 337 is not in vehicle-hull-accident-culprit-type. + if (raw == null || raw === 337) { + next[key] = FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID; + } else { + next[key] = raw; + } + continue; + } + if (key === "CustomerFaultPercent") { + const culpritType = + built.CulpritTypeId ?? FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID; + if (built.CustomerFaultPercent != null) { + next[key] = built.CustomerFaultPercent; + } else if (culpritType === 300) { + next[key] = 100; + } else { + next[key] = null; + } + continue; + } + next[key] = Object.prototype.hasOwnProperty.call(built, key) + ? built[key] + : null; + } + return next; +} + +/** Nested hull files/expertise use GEN.03 `Id` (کد رایانه), never `ClaimNo` (شماره پرونده). */ +export function fanavaranHullNestedClaimId(input: { + Id?: unknown; + ClaimNo?: unknown; + claimId?: unknown; + claimNo?: unknown; +}): number | null { + const id = input.claimId ?? input.Id; + if (typeof id === "number" && Number.isFinite(id) && id > 0) return id; + if (typeof id === "string" && /^\d+$/.test(id.trim())) return Number(id); + return null; +} diff --git a/src/claim-request-management/fanavaran-hull-expertise-lookups.ts b/src/claim-request-management/fanavaran-hull-expertise-lookups.ts new file mode 100644 index 0000000..6e834a8 --- /dev/null +++ b/src/claim-request-management/fanavaran-hull-expertise-lookups.ts @@ -0,0 +1,76 @@ +import { parseFanavaranId } from "src/lookups/fanavaran-last-car-policy"; + +export const FANAVARAN_HULL_DMG_KIND_CAPTION = "تخریب"; +export const FANAVARAN_HULL_DMG_COST_KIND_CAPTION = "مجموع لوازم"; + +/** Fallback when lookup fetch fails (Parsian `vehicle-hull-dmg-kind`). */ +export const FANAVARAN_DEFAULT_HULL_DMG_KIND_ID = 5485; +/** Fallback when lookup fetch fails (`vehicle-hull-dmg-cost-kinds` Id). */ +export const FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID = 1; + +export type FanavaranHullDmgAccessoryRow = { + Id?: unknown; + AccessoryKindId?: unknown; + AccessoryDesc?: unknown; +}; + +export function asLookupRows(value: unknown): Record[] { + if (!Array.isArray(value)) return []; + return value.filter( + (row): row is Record => + !!row && typeof row === "object" && !Array.isArray(row), + ); +} + +export function asHullDmgAccessoryRows(value: unknown): FanavaranHullDmgAccessoryRow[] { + return asLookupRows(value) as FanavaranHullDmgAccessoryRow[]; +} + +export function findLookupIdByCaption( + rows: unknown, + caption: string, +): number | null { + const target = caption.trim(); + for (const row of asLookupRows(rows)) { + if (String(row.Caption ?? "").trim() !== target) continue; + const id = parseFanavaranId(row.Id); + if (id != null) return id; + } + return null; +} + +function isFactoryDefaultAccessoryRow(row: FanavaranHullDmgAccessoryRow): boolean { + const desc = String(row.AccessoryDesc ?? "").trim(); + return ( + desc.includes("کليه قطعات فابريک") || + desc.includes("کلیه قطعات فابریک") || + desc.includes("کليه قطعات") || + desc.includes("فابريک") + ); +} + +/** + * GEN.06 VehicleHullAccessoryId from policy dmg-accessories. + * 1) Match AccessoryKindId to car-components part id. + * 2) Else factory bundle row (app default outer parts map here on Parsian). + */ +export function resolveVehicleHullAccessoryId( + accessories: FanavaranHullDmgAccessoryRow[], + accessoryKindId: number | null, +): number | null { + if (accessories.length === 0) return null; + + if (accessoryKindId != null) { + const exact = accessories.find( + (row) => parseFanavaranId(row.AccessoryKindId) === accessoryKindId, + ); + const exactId = parseFanavaranId(exact?.Id); + if (exactId != null) return exactId; + } + + const factory = accessories.find(isFactoryDefaultAccessoryRow); + const factoryId = parseFanavaranId(factory?.Id); + if (factoryId != null) return factoryId; + + return parseFanavaranId(accessories[0]?.Id); +} diff --git a/src/claim-request-management/fanavaran-hull-expertise.spec.ts b/src/claim-request-management/fanavaran-hull-expertise.spec.ts index 6513e45..6739bf3 100644 --- a/src/claim-request-management/fanavaran-hull-expertise.spec.ts +++ b/src/claim-request-management/fanavaran-hull-expertise.spec.ts @@ -5,9 +5,16 @@ import { pickHullVehicleIdentity, toFanavaranHullExpertiseDmgSection, toFanavaranHullExpertisePayload, + vehicleCurrentValueFromHullPolicyRecord, } from "./fanavaran-hull-expertise"; describe("fanavaran hull expertise", () => { + it("reads VehicleCurrentValue from hull policy VehicleValue", () => { + expect( + vehicleCurrentValueFromHullPolicyRecord({ VehicleValue: 11000000000 }), + ).toBe(11000000000); + }); + it("maps car-body inquiry identity onto GEN.06 vehicle fields", () => { const vehicle = pickHullVehicleIdentity({ parties: [ @@ -71,6 +78,9 @@ describe("fanavaran hull expertise", () => { desc: "bumper", wasteValue: 0, amount: 3000, + dmgKindId: 5485, + dmgCostKindId: 1, + vehicleHullAccessoryId: 3043330, }), ], }); @@ -89,10 +99,10 @@ describe("fanavaran hull expertise", () => { Desc: "bumper", WasteValue: 0, AccessoryKindId: 9, - DmgKindId: null, - VehicleHullAccessoryId: null, + DmgKindId: 5485, + VehicleHullAccessoryId: 3043330, DmgSectionCosts: [ - { Caption: "bumper", Amount: 3000, DmgCostKindId: null }, + { Caption: "bumper", Amount: 3000, DmgCostKindId: 1 }, ], }, ]); @@ -122,12 +132,18 @@ describe("fanavaran hull expertise", () => { desc: "سپر جلو", wasteValue: 0, amount: 11000000, + dmgKindId: 5485, + dmgCostKindId: 1, + vehicleHullAccessoryId: 3043330, }), toFanavaranHullExpertiseDmgSection({ partId: 11, desc: "آينه سمت راننده", wasteValue: 0, amount: 5000000, + dmgKindId: 5485, + dmgCostKindId: 1, + vehicleHullAccessoryId: 3043330, }), ], }); diff --git a/src/claim-request-management/fanavaran-hull-expertise.ts b/src/claim-request-management/fanavaran-hull-expertise.ts index 3854b8d..3c3ff80 100644 --- a/src/claim-request-management/fanavaran-hull-expertise.ts +++ b/src/claim-request-management/fanavaran-hull-expertise.ts @@ -13,6 +13,29 @@ export type FanavaranHullVehicleIdentity = { builtYear: number | null; }; +export function mergeHullVehicleIdentityFromFanavaranVehicle( + base: FanavaranHullVehicleIdentity, + vehicle: Record | null | undefined, +): FanavaranHullVehicleIdentity { + if (!vehicle) return base; + const readText = (key: string, fallback: string | null) => { + const raw = vehicle[key]; + if (raw == null || String(raw).trim() === "") return fallback; + return String(raw).trim(); + }; + return { + motorNo: readText("MotorNo", base.motorNo), + chassisNo: readText("ChassisNo", base.chassisNo), + vin: readText("VIN", base.vin), + plaqueNo: readText("PlaqueNo", base.plaqueNo), + plaqueSerial: readText("PlaqueSerial", base.plaqueSerial), + builtYear: + parseFanavaranId(vehicle.BuiltYear) ?? + parseFanavaranId(vehicle.builtYear) ?? + base.builtYear, + }; +} + export function pickHullVehicleIdentity( blame?: { parties?: unknown[] } | null, ): FanavaranHullVehicleIdentity { @@ -48,6 +71,24 @@ function text(value: unknown): string | null { return next ? next : null; } +export { + FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID, + FANAVARAN_DEFAULT_HULL_DMG_KIND_ID, + FANAVARAN_HULL_DMG_COST_KIND_CAPTION, + FANAVARAN_HULL_DMG_KIND_CAPTION, +} from "./fanavaran-hull-expertise-lookups"; + +/** GEN.06 VehicleCurrentValue from GET car/vehicle-hull-policies/{PolicyId} → VehicleValue. */ +export function vehicleCurrentValueFromHullPolicyRecord( + policy: Record | null | undefined, +): number | null { + if (!policy) return null; + const raw = + policy.VehicleValue ?? policy.vehicleValue ?? policy.VehicleCurrentValue; + const n = typeof raw === "number" ? raw : Number(raw); + return Number.isFinite(n) && n > 0 ? n : null; +} + export function toFanavaranHullExpertisePayload(input: { claimExpertId: number; dmgAssessmentDate: string; @@ -58,6 +99,8 @@ export function toFanavaranHullExpertisePayload(input: { dropAmount: number; vehicleCurrentValue: number | null; vehicle: FanavaranHullVehicleIdentity; + colorId?: number | null; + repairDuration?: number | null; dmgSections: Record[]; }): Record { return { @@ -74,7 +117,7 @@ export function toFanavaranHullExpertisePayload(input: { ComponentReplacementCost: input.componentReplacementCost, WasteValue: input.wasteValue, CarryAndRescueCost: null, - RepairDuration: null, + RepairDuration: input.repairDuration ?? null, VehicleCurrentValue: input.vehicleCurrentValue, WreckHighestValue: null, InspectionDeduction: null, @@ -82,7 +125,7 @@ export function toFanavaranHullExpertisePayload(input: { WentDistanceByExpert: null, IsDestruction: null, DropAmount: input.dropAmount, - ColorId: null, + ColorId: input.colorId ?? null, PlaqueDesignId: null, PlaqueCityId: null, AccidentPercent: null, @@ -96,19 +139,22 @@ export function toFanavaranHullExpertiseDmgSection(input: { desc: string; wasteValue: number; amount: number; + dmgKindId: number; + dmgCostKindId: number; + vehicleHullAccessoryId: number | null; }): Record { return { Count: 1, Desc: input.desc, WasteValue: input.wasteValue, AccessoryKindId: parseFanavaranId(input.partId), - DmgKindId: null, - VehicleHullAccessoryId: null, + DmgKindId: input.dmgKindId, + VehicleHullAccessoryId: input.vehicleHullAccessoryId, DmgSectionCosts: [ { Caption: input.desc, Amount: input.amount, - DmgCostKindId: null, + DmgCostKindId: input.dmgCostKindId, }, ], }; diff --git a/src/core/config/fanavaran-client.config.ts b/src/core/config/fanavaran-client.config.ts index c9403fc..4d8fb26 100644 --- a/src/core/config/fanavaran-client.config.ts +++ b/src/core/config/fanavaran-client.config.ts @@ -57,6 +57,8 @@ export interface FanavaranPayloadDefaults { * (Parsian: 29; Tejaratno proven: 2709). */ ExpertiseClaimExpertId: number; + /** GEN.06 hull RepairDuration when configured per tenant (optional). */ + HullExpertiseRepairDuration?: number | null; CompensationReferenceId: number; CulpritLicenceTypeId: number; CulpritTypeId: number; diff --git a/src/fanavaran/fanavaran-lookup.config.ts b/src/fanavaran/fanavaran-lookup.config.ts index 6309a4c..e358ee7 100644 --- a/src/fanavaran/fanavaran-lookup.config.ts +++ b/src/fanavaran/fanavaran-lookup.config.ts @@ -16,6 +16,26 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [ url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/accident-causes`, cacheFile: "accident-causes.json", }, + { + name: "vehicle-hull-accident-types", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/vehicle-hull-accident-types`, + cacheFile: "vehicle-hull-accident-types.json", + }, + { + name: "vehicle-hull-accident-culprit-type", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-accident-culprit-type`, + cacheFile: "vehicle-hull-accident-culprit-type.json", + }, + { + name: "vehicle-hull-dmg-kind", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-dmg-kind`, + cacheFile: "vehicle-hull-dmg-kind.json", + }, + { + name: "vehicle-hull-dmg-cost-kinds", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/vehicle-hull-dmg-cost-kinds`, + cacheFile: "vehicle-hull-dmg-cost-kinds.json", + }, { name: "accident-report-type", url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-report-type`, diff --git a/src/fanavaran/fanavaran-lookup.service.ts b/src/fanavaran/fanavaran-lookup.service.ts index f0dc436..2ffc112 100644 --- a/src/fanavaran/fanavaran-lookup.service.ts +++ b/src/fanavaran/fanavaran-lookup.service.ts @@ -277,6 +277,16 @@ export class FanavaranLookupService { return this.fetchFromFanavaran(clientKey, url, options); } + /** GEN.06 VehicleHullAccessoryId — requires hull PolicyId (کد رایانه بیمه‌نامه). */ + async vehicleHullDmgAccessoriesByPolicyId( + clientKey: FanavaranClientKey, + policyId: number, + options?: { contractIdOverride?: string }, + ): Promise { + const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/vehicle-hull-policies/${policyId}/dmg-accessories`; + return this.fetchFromFanavaran(clientKey, url, options); + } + async vehicleById( clientKey: FanavaranClientKey, vehicleId: number, diff --git a/src/lookups/lookups.controller.ts b/src/lookups/lookups.controller.ts index ea1d7d9..93d2784 100644 --- a/src/lookups/lookups.controller.ts +++ b/src/lookups/lookups.controller.ts @@ -36,6 +36,85 @@ export class LookupsController { return await this.lookupsService.getAccidentCauses(); } + @Get("vehicle-hull-accident-types") + @ApiOperation({ + summary: "Fanavaran GEN.03 hull accident type lookup", + description: + "Returns values for base-claim field AccidentTypeId from car/base-info/vehicle-hull-accident-types (not the Tejarat static accident-type list).", + }) + @ApiOkResponse({ + description: "Returns Fanavaran vehicle hull accident types", + schema: { type: "array", items: { type: "object" } }, + }) + async getVehicleHullAccidentTypes() { + return await this.lookupsService.getVehicleHullAccidentTypes(); + } + + @Get("vehicle-hull-accident-culprit-type") + @ApiOperation({ + summary: "Fanavaran GEN.03 hull culprit type lookup", + description: + "Returns values for base-claim field CulpritTypeId from car/code-list/vehicle-hull-accident-culprit-type (not accident-culprit-type used for ثالث).", + }) + @ApiOkResponse({ + description: "Returns Fanavaran vehicle hull accident culprit types", + schema: { type: "array", items: { type: "object" } }, + }) + async getVehicleHullAccidentCulpritType() { + return await this.lookupsService.getVehicleHullAccidentCulpritType(); + } + + @Get("vehicle-hull-dmg-kind") + @ApiOperation({ + summary: "Fanavaran GEN.06 hull damage kind lookup", + description: + "Returns values for DmgSections[].DmgKindId from car/code-list/vehicle-hull-dmg-kind.", + }) + @ApiOkResponse({ + description: "Returns Fanavaran vehicle hull damage kinds", + schema: { type: "array", items: { type: "object" } }, + }) + async getVehicleHullDmgKind() { + return await this.lookupsService.getVehicleHullDmgKind(); + } + + @Get("vehicle-hull-dmg-cost-kinds") + @ApiOperation({ + summary: "Fanavaran GEN.06 hull damage cost kind lookup", + description: + "Returns values for DmgSections[].DmgSectionCosts[].DmgCostKindId from car/base-info/vehicle-hull-dmg-cost-kinds.", + }) + @ApiOkResponse({ + description: "Returns Fanavaran vehicle hull damage cost kinds", + schema: { type: "array", items: { type: "object" } }, + }) + async getVehicleHullDmgCostKinds() { + return await this.lookupsService.getVehicleHullDmgCostKinds(); + } + + @Get("vehicle-hull-dmg-accessories/:policyId") + @ApiOperation({ + summary: "Fanavaran GEN.06 hull policy damage accessories", + description: + "Returns rows for DmgSections[].VehicleHullAccessoryId from car/vehicle-hull-policies/{policyId}/dmg-accessories. PolicyId is required (Fanavaran: کد رایانه ریسورس).", + }) + @ApiParam({ + name: "policyId", + description: "Fanavaran hull policy Id (same as GEN.03 PolicyId)", + example: 13764610, + }) + @ApiOkResponse({ + description: "Returns Fanavaran vehicle hull damage accessories for the policy", + schema: { type: "array", items: { type: "object" } }, + }) + async getVehicleHullDmgAccessoriesByPolicyId( + @Param("policyId", ParseIntPipe) policyId: number, + ) { + return await this.lookupsService.getVehicleHullDmgAccessoriesByPolicyId( + policyId, + ); + } + @Get("accident-report-type") @ApiOkResponse({ description: "Returns accident report type lookup data", diff --git a/src/lookups/lookups.service.ts b/src/lookups/lookups.service.ts index 550f857..8951b5a 100644 --- a/src/lookups/lookups.service.ts +++ b/src/lookups/lookups.service.ts @@ -106,6 +106,36 @@ export class LookupsService { return await this.getClientRemoteLookup("accident-causes"); } + async getVehicleHullAccidentTypes(): Promise { + return await this.getClientRemoteLookup("vehicle-hull-accident-types"); + } + + async getVehicleHullAccidentCulpritType(): Promise { + return await this.getClientRemoteLookup("vehicle-hull-accident-culprit-type"); + } + + async getVehicleHullDmgKind(): Promise { + return await this.getClientRemoteLookup("vehicle-hull-dmg-kind"); + } + + async getVehicleHullDmgCostKinds(): Promise { + return await this.getClientRemoteLookup("vehicle-hull-dmg-cost-kinds"); + } + + async getVehicleHullDmgAccessoriesByPolicyId(policyId: number): Promise { + const clientKey = this.activeClientKey(); + return this.fanavaranLookupService.vehicleHullDmgAccessoriesByPolicyId( + clientKey, + policyId, + { + contractIdOverride: resolveFanavaranProductContractId( + clientKey, + "car-body", + ), + }, + ); + } + async getAccidentReportType(): Promise { return await this.getClientRemoteLookup("accident-report-type"); } From ebfb4385deeb8f587a2671092206b198abdffd08 Mon Sep 17 00:00:00 2001 From: "s.hajizadeh" Date: Wed, 16 Sep 2026 16:26:58 +0330 Subject: [PATCH 2/2] 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 --- .../claim-request-management.service.ts | 109 ++++++++++++++++-- .../fanavaran-file-role-expert-ids.spec.ts | 57 +++++++++ .../fanavaran-file-role-expert-ids.ts | 90 +++++++++++++++ .../request-management.service.ts | 23 ++++ 4 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 src/fanavaran/fanavaran-file-role-expert-ids.spec.ts create mode 100644 src/fanavaran/fanavaran-file-role-expert-ids.ts 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,