From acb5d2a6827c6c3e2e0afd0bdfecc4334d90ace5 Mon Sep 17 00:00:00 2001 From: "s.hajizadeh" Date: Wed, 16 Sep 2026 16:05:54 +0330 Subject: [PATCH] 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"); }