From 0b0a1dfa138266a328db644c9297dc01ea4187b7 Mon Sep 17 00:00:00 2001 From: "s.hajizadeh" Date: Tue, 15 Sep 2026 16:36:13 +0330 Subject: [PATCH] third party update , optional body is done --- .../claim-request-management.service.ts | 357 ++++++++++-------- .../fanavaran-claim-product.spec.ts | 16 + .../fanavaran-claim-product.ts | 29 ++ src/fanavaran/fanavaran.controller.ts | 47 ++- src/lookups/fanavaran-last-car-policy.spec.ts | 36 ++ src/lookups/fanavaran-last-car-policy.ts | 83 ++++ 6 files changed, 386 insertions(+), 182 deletions(-) diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index e5a9640..9f7eb57 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -37,8 +37,11 @@ import { ClaimRequestManagementDbService } from "./entites/db-service/claim-requ import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service"; import { fanavaranClaimReferences } from "./fanavaran-claim-references"; import { + FANAVARAN_HULL_NO_DAMAGE_CASE_REASON, fanavaranClaimProductFromBlameType, fanavaranClaimsBaseUrl, + fanavaranClaimStageUrl, + fanavaranHasDamageCaseStage, fanavaranInsuranceLineIdForProduct, fanavaranPartyInquirySources, isFanavaranSubmitSupportedBlameType, @@ -228,18 +231,20 @@ import { import { asObjectRecord, asVehicleInquiryRows, - canReuseCachedFanavaranVehicleIds, collectPartyVinCandidates, completePlaqueParts, - fanavaranIdOrConfigDefault, - fanavaranPayloadMatchesDamagedIdentity, fanavaranVehicleMatchesDamagedIdentity, normalizeVin, + applyFanavaranManualPayloadOverrides, + FANAVARAN_BASE_MANUAL_OVERRIDE_KEYS, + FANAVARAN_DAMAGE_MANUAL_OVERRIDE_KEYS, + FANAVARAN_EXPERTISE_MANUAL_OVERRIDE_KEYS, parseFanavaranId, pickFanavaranPlaqueLookupFields, pickFanavaranVehicleFromInquiry, pickVehicleId, pickVinFromPartyVehicle, + resolveDamageCasePlaqueLookupIds, selectFanavaranVehicleForDamageCase, type FanavaranDamagedVehicleIdentity, } from "src/lookups/fanavaran-last-car-policy"; @@ -4336,17 +4341,6 @@ export class ClaimRequestManagementService { } const cachedDamage = (input.claimCase as any)?.fanavaranSync?.damageCase ?? {}; - const lastDamagePayload = asObjectRecord(cachedDamage.lastPayload); - const cacheMatchesIdentity = fanavaranPayloadMatchesDamagedIdentity( - lastDamagePayload, - damagedIdentity, - ); - const canReuseCachedPlaque = - cacheMatchesIdentity && - canReuseCachedFanavaranVehicleIds(cachedDamage.plaqueKindSource); - const canReuseCachedVehicleKind = - cacheMatchesIdentity && - canReuseCachedFanavaranVehicleIds(cachedDamage.vehicleKindSource); this.logger.log( `[buildFanavaranDamageCasePayload] identity VIN=${damagedIdentity.vin ?? "none"} chassis=${damagedIdentity.chassis ?? "none"} plate=${damagedIdentity.plaque ? `${damagedIdentity.plaque.right}${damagedIdentity.plaque.letter}${damagedIdentity.plaque.left}/${damagedIdentity.plaque.serial}` : "none"} CI=${damagedIdentity.policyCINumber ?? "none"} VIN candidates=${damagedVinCandidates.join(",") || "none"}`, @@ -4358,63 +4352,25 @@ export class ClaimRequestManagementService { product, ); const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle); - const inquiryVehicle = - asObjectRecord(inquiryMapped) ?? asObjectRecord(inquiryRaw); - const inquiryPlaque = fanavaranVehicleMatchesDamagedIdentity( - inquiryVehicle, - damagedIdentity, - ) - ? pickFanavaranPlaqueLookupFields(inquiryVehicle) - : pickFanavaranPlaqueLookupFields(null); - const plaqueKindFromApi = this.firstPositiveFanavaranId( - apiPlaque.kindId, - inquiryPlaque.kindId, - canReuseCachedPlaque ? cachedDamage.plaqueKindId : null, - canReuseCachedPlaque ? lastDamagePayload?.PlaqueKindId : null, - ); - const plaqueSampleFromApi = this.firstPositiveFanavaranId( - apiPlaque.sampleId, - inquiryPlaque.sampleId, - canReuseCachedPlaque ? cachedDamage.plaqueSampleId : null, - canReuseCachedPlaque ? lastDamagePayload?.PlaqueSampleId : null, - ); - const plaqueKindId = fanavaranIdOrConfigDefault( - plaqueKindFromApi, - input.defaults.PlaqueKindId, - ); - const plaqueSampleId = fanavaranIdOrConfigDefault( - plaqueSampleFromApi, - input.defaults.PlaqueSampleId, - ); - const plaqueKindSource = - plaqueKindFromApi != null ? "vehicle-inquiry" : "config-default"; - const plaqueSampleSource = - plaqueSampleFromApi != null ? "vehicle-inquiry" : "config-default"; - if (plaqueKindFromApi == null) { + const plaqueLookup = resolveDamageCasePlaqueLookupIds(apiVehicle); + const plaqueKindId = plaqueLookup.kindId; + const plaqueSampleId = plaqueLookup.sampleId; + const plaqueKindSource = plaqueLookup.source; + const plaqueSampleSource = plaqueLookup.source; + if (plaqueLookup.source !== "vehicle-inquiry") { this.logger.log( - `[buildFanavaranDamageCasePayload] PlaqueKindId API miss; using config default ${plaqueKindId}`, - ); - } - if (plaqueSampleFromApi == null) { - this.logger.log( - `[buildFanavaranDamageCasePayload] PlaqueSampleId API miss; using config default ${plaqueSampleId}`, + `[buildFanavaranDamageCasePayload] PlaqueKindId not on a matched Fanavaran vehicle; using national-plate default ${plaqueKindId}/${plaqueSampleId}`, ); } const builtYear = parseFanavaranId(apiVehicle?.BuiltYear) ?? builtYearFromInquiry; const carType = input.claimCase?.vehicle?.carType as string | undefined; - let vehicleKindId = this.firstPositiveFanavaranId( - apiVehicle?.VehicleKindId, - canReuseCachedVehicleKind ? cachedDamage.vehicleKindId : null, - canReuseCachedVehicleKind ? lastDamagePayload?.VehicleKindId : null, - ); + let vehicleKindId = this.firstPositiveFanavaranId(apiVehicle?.VehicleKindId); let vehicleKindSource: "vehicle-inquiry" | "car-type-lookup" | null = parseFanavaranId(apiVehicle?.VehicleKindId) != null ? "vehicle-inquiry" - : canReuseCachedVehicleKind - ? "vehicle-inquiry" - : null; + : null; if (vehicleKindId == null) { vehicleKindId = await this.resolveVehicleKindId( input.clientKey, @@ -4423,84 +4379,62 @@ export class ClaimRequestManagementService { if (vehicleKindId != null) vehicleKindSource = "car-type-lookup"; } - // Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload - let driverFanavaranId = this.firstPositiveFanavaranId( - cachedDamage.driverId, - cachedDamage.otherPersonId, - person.fanavaranDriverId, - lastDamagePayload?.DriverId, + let driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase( + input.clientKey, + input.blameCase?.parties ?? [], + person, + input.claimCase, ); let otherPersonId = this.firstPositiveFanavaranId(cachedDamage.otherPersonId); if (driverFanavaranId) { this.logger.log( - `[buildFanavaranDamageCasePayload] Using CACHED DriverId=${driverFanavaranId}`, + `[buildFanavaranDamageCasePayload] DriverId=${driverFanavaranId} from live parties inquiry`, ); - } else { - driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase( - input.clientKey, - input.blameCase?.parties ?? [], + } else if (otherPersonId) { + driverFanavaranId = otherPersonId; + this.logger.log( + `[buildFanavaranDamageCasePayload] Parties inquiry missed; reusing GEN.44 otherPersonId=${otherPersonId}`, + ); + } else if (input.registerMissingPerson) { + const registered = await this.registerFanavaranOtherPerson({ + clientKey: input.clientKey, person, - input.claimCase, - ); - - if (!driverFanavaranId && input.registerMissingPerson) { - const registered = await this.registerFanavaranOtherPerson({ - clientKey: input.clientKey, - person, - claimCaseId: input.claimCase?._id - ? String(input.claimCase._id) - : undefined, - }); - if (registered != null) { - driverFanavaranId = registered; - otherPersonId = registered; - } - } - - // Persist the resolved ID back to the blame case party for future use - if (driverFanavaranId && input.blameCase?._id && damagedParty) { - const partyIndex = (input.blameCase.parties ?? []).findIndex( - (p: any) => p === damagedParty || - String(p?.person?.userId ?? "") === - String(damagedParty?.person?.userId ?? ""), - ); - if (partyIndex >= 0) { - await this.blameRequestDbService.findByIdAndUpdate( - String(input.blameCase._id), - { $set: { [`parties.${partyIndex}.person.fanavaranDriverId`]: driverFanavaranId } }, - ); - this.logger.log( - `[buildFanavaranDamageCasePayload] CACHED DriverId=${driverFanavaranId} on blameCase=${input.blameCase._id} party[${partyIndex}]`, - ); - } + claimCaseId: input.claimCase?._id + ? String(input.claimCase._id) + : undefined, + }); + if (registered != null) { + driverFanavaranId = registered; + otherPersonId = registered; } } - let insuranceCorpId = - cachedDamage.insuranceCorpId != null - ? Number(cachedDamage.insuranceCorpId) - : null; - if (insuranceCorpId == null) { - insuranceCorpId = await this.fanavaranLookupService.resolveInsuranceCorpId( + if (driverFanavaranId && input.blameCase?._id && damagedParty) { + const partyIndex = (input.blameCase.parties ?? []).findIndex( + (p: any) => p === damagedParty || + String(p?.person?.userId ?? "") === + String(damagedParty?.person?.userId ?? ""), + ); + if (partyIndex >= 0) { + await this.blameRequestDbService.findByIdAndUpdate( + String(input.blameCase._id), + { $set: { [`parties.${partyIndex}.person.fanavaranDriverId`]: driverFanavaranId } }, + ); + this.logger.log( + `[buildFanavaranDamageCasePayload] stored DriverId=${driverFanavaranId} on blameCase=${input.blameCase._id} party[${partyIndex}]`, + ); + } + } + + const insuranceCorpId = + await this.fanavaranLookupService.resolveInsuranceCorpId( input.clientKey, ); - } - const cachedUsedId = - cacheMatchesIdentity && cachedDamage.accidentVehicleUsedId != null - ? Number(cachedDamage.accidentVehicleUsedId) - : null; - const cachedUsedSource = cachedDamage.accidentVehicleUsedSource; - const canReuseApiUsedId = - cachedUsedId != null && - cachedUsedId > 0 && - cachedUsedSource === "vehicle-inquiry"; - let accidentVehicleUsedId: number | null = canReuseApiUsedId - ? cachedUsedId - : null; + let accidentVehicleUsedId: number | null = null; let usedSource: "vin-inquiry" | "policy-vehicle" | "vehicle-inquiry" | null = - canReuseApiUsedId ? "vehicle-inquiry" : null; + null; if (accidentVehicleUsedId == null && apiVehicle) { const fromDamagedVehicle = await this.resolveAccidentVehicleUsedFromVehicleRecord( @@ -4597,10 +4531,7 @@ export class ClaimRequestManagementService { PolicyCINumber: policyCINumber, }; - const persistedPayload = this.keepPreviousPositiveIds(lastDamagePayload, payload, [ - "DriverId", - "InsuranceCorpId", - ]); + const persistedPayload = payload; // Persist Fanavaran-sourced IDs + last payload for reuse on later preview/submit if (input.claimCase?._id) { @@ -5502,12 +5433,9 @@ export class ClaimRequestManagementService { /** * Build Fanavaran GEN.03 payload from local claim/blame data. * - * Fanavaran-sourced fields (PolicyId) use resolve-once caching: - * - If `fanavaranSync.baseClaim.policyId` exists → reuse (no policy HTTP). - * - Else → live inquiry-my-policies once, then persist policyId + lastPayload. - * + * HTTP preview/submit always pass forceRefreshPolicy + forceRefreshUsedId, + * so PolicyId and AccidentVehicleUsedId are re-inquired every call. * Auth still uses the shared midnight-TTL token cache (not a new login per preview). - * Pass `forceRefreshPolicy: true` to bust the PolicyId cache. */ async previewFanavaranSubmitV2( claimCaseId: string, @@ -5577,7 +5505,7 @@ export class ClaimRequestManagementService { }, failureReason: null as string | null, note: - "PolicyId is resolve-once: first preview/submit may call Fanavaran policy inquiry; later calls reuse fanavaranSync.baseClaim.policyId. Auth token is shared until Tehran midnight.", + "HTTP preview/submit always re-inquire PolicyId and AccidentVehicleUsedId. Auth token is shared until Tehran midnight.", }; const claimCase = await this.claimCaseDbService.findById(claimCaseId); @@ -5911,6 +5839,21 @@ export class ClaimRequestManagementService { ); } + const product = fanavaranClaimProductFromBlameType(blameCase.type); + if (!fanavaranHasDamageCaseStage(product)) { + return { + clientKey, + claimCaseId, + claimId: claimCase.claimId ?? null, + dmgCaseId: claimCase.dmgCaseId ?? null, + submitUrl: null, + skipped: true, + skipReason: FANAVARAN_HULL_NO_DAMAGE_CASE_REASON, + warning: FANAVARAN_HULL_NO_DAMAGE_CASE_REASON, + payload: null, + }; + } + const payload = await this.buildFanavaranDamageCasePayload({ claimCase, blameCase, @@ -5935,7 +5878,7 @@ export class ClaimRequestManagementService { claimId: claimCase.claimId ?? null, dmgCaseId: claimCase.dmgCaseId ?? null, submitUrl: claimCase.claimId - ? `${fanavaranClaimsBaseUrl(fanavaranClaimProductFromBlameType(blameCase.type))}/${claimCase.claimId}/dmg-cases` + ? fanavaranClaimStageUrl(product, "damage-case", claimCase.claimId) : null, warning: warnings.length ? warnings.join(" ") : undefined, payload, @@ -6775,15 +6718,14 @@ export class ClaimRequestManagementService { 0, ); const submittedAt = reply?.submittedAt ?? new Date(); + const product = await this.resolveFanavaranClaimProduct( + input.claimCase?._id ? String(input.claimCase._id) : undefined, + ); - return { - replyKey: active?.replyKey, - warnings, - payload: { + const payload: Record = { ClaimExpertId: claimExpertId, ComponentReplacementCost: componentReplacementCost, DmgAssessmentDate: this.convertToPersianDate(submittedAt), - DmgCaseId: input.claimCase.dmgCaseId ?? null, InspectionPlaceId: inspectionPlaceId, InspectionTime: this.getTime24Hour(submittedAt), RepairWage: repairWage, @@ -6795,18 +6737,28 @@ export class ClaimRequestManagementService { DamagedVehicleCurrentPrice: this.parseFanavaranMoney(priceDrop?.carPrice) || null, DmgSections: dmgSections, - }, + }; + if (fanavaranHasDamageCaseStage(product)) { + payload.DmgCaseId = input.claimCase.dmgCaseId ?? null; + } + + return { + replyKey: active?.replyKey, + warnings, + payload, }; } private assertFanavaranExpertisePayloadReady( payload: Record, warnings: string[], + options?: { requireDmgCaseId?: boolean }, ): void { const sections = Array.isArray(payload.DmgSections) ? payload.DmgSections : []; - if (!payload.DmgCaseId) warnings.push("DmgCaseId is required."); + if (options?.requireDmgCaseId !== false && !payload.DmgCaseId) + warnings.push("DmgCaseId is required."); if (!payload.InspectionPlaceId) warnings.push("InspectionPlaceId is required."); if (!payload.DropAmountStatus) @@ -6876,17 +6828,19 @@ export class ClaimRequestManagementService { ); const claimCase = await this.claimCaseDbService.findById(claimCaseId); if (!claimCase) throw new NotFoundException("Claim case not found"); - let payload: Record; - if (bodyOverride) { - payload = bodyOverride; - } else { - const built = await this.buildFanavaranExpertisePayload({ - claimCase, - clientKey, - }); - payload = built.payload; - this.assertFanavaranExpertisePayloadReady(payload, built.warnings); - } + const product = await this.resolveFanavaranClaimProduct(claimCaseId); + const built = await this.buildFanavaranExpertisePayload({ + claimCase, + clientKey, + }); + const payload = applyFanavaranManualPayloadOverrides( + built.payload, + bodyOverride, + FANAVARAN_EXPERTISE_MANUAL_OVERRIDE_KEYS, + ); + this.assertFanavaranExpertisePayloadReady(payload, built.warnings, { + requireDmgCaseId: fanavaranHasDamageCaseStage(product), + }); return await this.executeFanavaranExpertiseSubmit( claimCaseId, clientKey, @@ -6910,7 +6864,11 @@ export class ClaimRequestManagementService { auditSource: FanavaranAuditSource, ): Promise { let claimCase = await this.claimCaseDbService.findById(claimCaseId); - if (!claimCase?.claimId || claimCase.dmgCaseId == null) { + const product = await this.resolveFanavaranClaimProduct(claimCaseId); + if ( + !claimCase?.claimId || + (fanavaranHasDamageCaseStage(product) && claimCase.dmgCaseId == null) + ) { await this.ensureFanavaranDamageCase( claimCaseId, clientKey, @@ -7063,7 +7021,8 @@ export class ClaimRequestManagementService { await this.autoSubmitToFanavaranV2OnClaimCreated(claimCaseId); claimCase = await this.claimCaseDbService.findById(claimCaseId); } - if (!claimCase?.dmgCaseId) { + const product = await this.resolveFanavaranClaimProduct(claimCaseId); + if (fanavaranHasDamageCaseStage(product) && !claimCase?.dmgCaseId) { await this.autoSubmitFanavaranDamageCaseOnOuterPartsSelected( claimCaseId, claimCase?.damage?.selectedParts ?? [], @@ -7075,7 +7034,9 @@ export class ClaimRequestManagementService { claimCase, clientKey, }); - this.assertFanavaranExpertisePayloadReady(payload, warnings); + this.assertFanavaranExpertisePayloadReady(payload, warnings, { + requireDmgCaseId: fanavaranHasDamageCaseStage(product), + }); const fanavaranResponse = await this.executeFanavaranExpertiseSubmit( claimCaseId, @@ -7245,7 +7206,11 @@ export class ClaimRequestManagementService { clientKey: FanavaranClientKey, auditSource: FanavaranAuditSource, selectedParts?: unknown[], - ): Promise<{ claimId: number; dmgCaseId: number; created: boolean }> { + ): Promise<{ + claimId: number; + dmgCaseId: number | null; + created: boolean; + }> { await this.ensureFanavaranBaseClaim(claimCaseId, clientKey, auditSource); const existing = await this.claimCaseDbService.findById(claimCaseId); @@ -7257,6 +7222,20 @@ export class ClaimRequestManagementService { }; } + const product = await this.resolveFanavaranClaimProduct(claimCaseId); + if (!fanavaranHasDamageCaseStage(product)) { + if (existing?.claimId == null) { + throw new BadRequestException( + "Fanavaran claimId is required before the next hull stage.", + ); + } + return { + claimId: Number(existing.claimId), + dmgCaseId: null, + created: false, + }; + } + this.logger.log( `[Fanavaran ${clientKey}] Soft-ensuring damage case before next stage claimCaseId=${claimCaseId}`, ); @@ -7396,7 +7375,7 @@ export class ClaimRequestManagementService { ); } } - if (claimCase.dmgCaseId != null && !bodyOverride) { + if (claimCase.dmgCaseId != null) { this.logger.log( `[executeFanavaranDamageCaseSubmit] Already have dmgCaseId=${claimCase.dmgCaseId}; skipping Fanavaran POST`, ); @@ -7422,7 +7401,34 @@ export class ClaimRequestManagementService { ); } - const payload = bodyOverride ?? await this.buildFanavaranDamageCasePayload({ + const product = fanavaranClaimProductFromBlameType(blameCase.type); + const url = fanavaranClaimStageUrl( + product, + "damage-case", + claimCase.claimId, + ); + if (url == null) { + this.logger.log( + `[executeFanavaranDamageCaseSubmit] claimCaseId=${claimCaseId} claimId=${claimCase.claimId} skipping hull dmg-cases`, + ); + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + "fanavaranSync.damageCase": { + status: "skipped", + lastTriedAt: new Date(), + lastError: FANAVARAN_HULL_NO_DAMAGE_CASE_REASON, + claimId: claimCase.claimId, + }, + }, + }); + return { + skipped: true, + skipReason: FANAVARAN_HULL_NO_DAMAGE_CASE_REASON, + claimId: claimCase.claimId, + }; + } + + const builtPayload = await this.buildFanavaranDamageCasePayload({ claimCase, blameCase, selectedParts: selectedParts ?? claimCase.damage?.selectedParts ?? [], @@ -7430,11 +7436,15 @@ export class ClaimRequestManagementService { defaults: profile.defaults, registerMissingPerson: true, }); + const payload = applyFanavaranManualPayloadOverrides( + builtPayload, + bodyOverride, + FANAVARAN_DAMAGE_MANUAL_OVERRIDE_KEYS, + ); // Guardrail: never send empty/null licence fields even if manual overrides arrive. payload.LicenceNo = this.ensureFanavaranNonEmptyLicenceNo(payload.LicenceNo); - const url = `${fanavaranClaimsBaseUrl(fanavaranClaimProductFromBlameType(blameCase.type))}/${claimCase.claimId}/dmg-cases`; this.logger.log( `[executeFanavaranDamageCaseSubmit] claimCaseId=${claimCaseId} claimId=${claimCase.claimId} payload.DriverId=${payload.DriverId ?? "NULL"} payload keys=${Object.keys(payload).join(", ")}`, ); @@ -7583,6 +7593,16 @@ export class ClaimRequestManagementService { clientKey, selectedParts, ); + if (fanavaranResponse?.skipped) { + return { + attempted: false, + submitted: false, + skipped: true, + skipReason: + fanavaranResponse.skipReason ?? FANAVARAN_HULL_NO_DAMAGE_CASE_REASON, + claimId: claimCase.claimId, + }; + } const dmgCaseId = fanavaranResponse?.Id; await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { @@ -8208,7 +8228,7 @@ export class ClaimRequestManagementService { ); const existing = await this.claimCaseDbService.findById(claimCaseId); - if (existing?.claimId != null && !bodyOverride) { + if (existing?.claimId != null) { this.logger.log( `${logPrefix} Base claim already exists claimId=${existing.claimId}; skipping Fanavaran POST`, ); @@ -8227,14 +8247,19 @@ export class ClaimRequestManagementService { claimCaseId, }; - const fanavaranData = - bodyOverride ?? - (await this.previewFanavaranSubmitV2(claimCaseId, clientKey, { + const built = + await this.previewFanavaranSubmitV2(claimCaseId, clientKey, { requirePolicyId: true, auditSession, persistPayload: true, + forceRefreshPolicy: true, forceRefreshUsedId: true, - })); + }); + const fanavaranData = applyFanavaranManualPayloadOverrides( + built, + bodyOverride, + FANAVARAN_BASE_MANUAL_OVERRIDE_KEYS, + ); // Guardrail: never send empty/null licence fields even if manual overrides arrive. fanavaranData.CulpritLicenceNo = this.ensureFanavaranNonEmptyLicenceNo( diff --git a/src/claim-request-management/fanavaran-claim-product.spec.ts b/src/claim-request-management/fanavaran-claim-product.spec.ts index 3ea89fb..4fc9dd6 100644 --- a/src/claim-request-management/fanavaran-claim-product.spec.ts +++ b/src/claim-request-management/fanavaran-claim-product.spec.ts @@ -3,6 +3,8 @@ import { PartyRole } from "src/request-management/entities/schema/partyRole.enum import { fanavaranClaimProductFromBlameType, fanavaranClaimsBaseUrl, + fanavaranClaimStageUrl, + fanavaranHasDamageCaseStage, fanavaranPartyInquirySources, isFanavaranSubmitSupportedBlameType, pickCarBodyPolicyNationalCode, @@ -28,6 +30,20 @@ describe("fanavaran claim product", () => { ); }); + it("does not POST hull damage to the missing dmg-cases resource", () => { + expect(fanavaranHasDamageCaseStage("car-body")).toBe(false); + expect(fanavaranClaimStageUrl("car-body", "damage-case", 5023617)).toBeNull(); + expect(fanavaranClaimStageUrl("car-body", "attachments", 5023617)).toBe( + "https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/5023617/files", + ); + expect(fanavaranClaimStageUrl("car-body", "expertise", 5023617)).toBe( + "https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/5023617/expertise", + ); + expect(fanavaranClaimStageUrl("third-party", "damage-case", 1)).toBe( + "https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/third-party-car-financial-claims/1/dmg-cases", + ); + }); + it("allows Fanavaran submit for both car products", () => { expect(isFanavaranSubmitSupportedBlameType("THIRD_PARTY")).toBe(true); expect(isFanavaranSubmitSupportedBlameType("CAR_BODY")).toBe(true); diff --git a/src/claim-request-management/fanavaran-claim-product.ts b/src/claim-request-management/fanavaran-claim-product.ts index f6b71ba..51c70aa 100644 --- a/src/claim-request-management/fanavaran-claim-product.ts +++ b/src/claim-request-management/fanavaran-claim-product.ts @@ -46,6 +46,35 @@ export function fanavaranClaimsBaseUrl( return `${FANAVARAN_CLAIMS_HOST}/${fanavaranClaimsResourcePath(product)}`; } +export type FanavaranClaimStage = "damage-case" | "attachments" | "expertise"; + +/** Hull (بدنه) has no GEN.12 dmg-cases resource. Proven 2026-09-15: Feature:url(.../vehicle-hull-claims/{id}/dmg-cases) not found. Files and expertise do exist. */ +export const FANAVARAN_HULL_NO_DAMAGE_CASE_REASON = + "Fanavaran vehicle-hull-claims has no dmg-cases resource. Hull is first-party (one damaged vehicle on the base claim). Continue with files and expertise."; + +export function fanavaranHasDamageCaseStage( + product: FanavaranClaimProduct, +): boolean { + return product === "third-party"; +} + +export function fanavaranClaimStageUrl( + product: FanavaranClaimProduct, + stage: FanavaranClaimStage, + claimId: number | string, +): string | null { + if (stage === "damage-case" && !fanavaranHasDamageCaseStage(product)) { + return null; + } + const suffix = + stage === "damage-case" + ? "dmg-cases" + : stage === "attachments" + ? "files" + : "expertise"; + return `${fanavaranClaimsBaseUrl(product)}/${claimId}/${suffix}`; +} + export function fanavaranInsuranceLineIdForProduct( product: FanavaranClaimProduct, ): number { diff --git a/src/fanavaran/fanavaran.controller.ts b/src/fanavaran/fanavaran.controller.ts index de58a13..e0daa8c 100644 --- a/src/fanavaran/fanavaran.controller.ts +++ b/src/fanavaran/fanavaran.controller.ts @@ -10,6 +10,7 @@ import { } from "@nestjs/common"; import { ApiBearerAuth, + ApiBody, ApiOperation, ApiParam, ApiQuery, @@ -57,7 +58,7 @@ export class FanavaranController { @ApiOperation({ summary: "Preview Fanavaran base claim create payload", description: - "Builds the GEN.03 payload from local claim/blame data. Fanavaran-sourced PolicyId is resolve-once: first call may inquire and caches on the claim; later calls reuse the cache. Auth token is shared until Asia/Tehran midnight. Pass forceRefreshPolicy=true to re-inquire.", + "GET has no request body. Always rebuilds GEN.03 from live policy + VIN/vehicle inquiries. Does not reuse fanavaranSync lastPayload / PolicyId / UsedId cache. Auth token is still shared until Asia/Tehran midnight.", }) @ApiParam({ name: "client", @@ -76,34 +77,30 @@ export class FanavaranController { @ApiQuery({ name: "forceRefreshPolicy", required: false, - description: - "When true, ignores cached PolicyId and performs a live Fanavaran policy inquiry again. Do not pass this from normal UI loads.", + deprecated: true, + description: "Ignored. Preview always re-inquires PolicyId.", }) @ApiQuery({ name: "resolvePolicy", required: false, deprecated: true, - description: - "Deprecated. Ignored for cache-busting. PolicyId is resolve-once from fanavaranSync.baseClaim.policyId; use forceRefreshPolicy=true only to re-inquire.", + description: "Ignored. Preview always re-inquires PolicyId.", }) async preview( @Param("client") client: string, @Param("claimCaseId") claimCaseId: string, @Query("debug") debug?: string, - @Query("forceRefreshPolicy") forceRefreshPolicy?: string, + @Query("forceRefreshPolicy") _forceRefreshPolicy?: string, @Query("resolvePolicy") _resolvePolicy?: string, ) { const clientKey = this.parseClientParam(client); - // IMPORTANT: resolvePolicy must NOT force a live inquiry. Older UI clients - // send resolvePolicy=true on every preview load; that used to defeat the - // PolicyId cache and re-Login Fanavaran on every click. return await this.claimRequestManagementService.previewFanavaranSubmitV2( claimCaseId, clientKey, { debug: debug === "1" || debug === "true", - forceRefreshPolicy: - forceRefreshPolicy === "1" || forceRefreshPolicy === "true", + forceRefreshPolicy: true, + forceRefreshUsedId: true, requirePolicyId: false, }, ); @@ -113,7 +110,7 @@ export class FanavaranController { @ApiOperation({ summary: "Submit Fanavaran base claim create request", description: - "Authenticates with the selected tenant credentials and submits the GEN.03 base claim create request. Stores returned Id as claimId and ClaimNo when present.", + "Swagger JSON body is optional. Always rebuilds GEN.03 from live inquiries, then POSTs that rebuilt payload. Pasting a previous preview JSON does not skip inquiry. Only user-entry keys are applied from the body (EstimateAmount, licence, dates, address, police report). PolicyId and AccidentVehicleUsedId in pasted JSON are ignored.", }) @ApiParam({ name: "client", @@ -124,6 +121,12 @@ export class FanavaranController { name: "claimCaseId", description: "Claim case MongoDB ObjectId", }) + @ApiBody({ + required: false, + description: + "Optional. Paste previous preview JSON if you want to tweak user-entry fields. Lookup ids in that JSON are ignored.", + schema: { type: "object", additionalProperties: true, example: {} }, + }) async submit( @Param("client") client: string, @Param("claimCaseId") claimCaseId: string, @@ -141,7 +144,7 @@ export class FanavaranController { @ApiOperation({ summary: "Preview Fanavaran damage-case payload", description: - "Builds the GEN.12 damaged vehicle/person case payload without calling Fanavaran. Requires selected damaged parts; submit requires a Fanavaran claimId.", + "GET has no request body. Always rebuilds GEN.12 from live VIN/vehicle/driver inquiries. Does not reuse lastPayload / plaqueKindId / DriverId cache.", }) @ApiParam({ name: "client", @@ -167,7 +170,7 @@ export class FanavaranController { @ApiOperation({ summary: "Submit Fanavaran damage-case request", description: - "Submits the GEN.12 dmg-cases request. If base claim (claimId) is missing, soft-ensures GEN.03 base claim first, then submits damage. Skips when dmgCaseId already exists.", + "Swagger JSON body is optional. Always rebuilds GEN.12 from live VIN/vehicle/driver inquiries, then POSTs that rebuilt payload. Pasting a previous preview JSON does not skip inquiry. Only Desc, EstimateAmount, LicenceNo, LicenceIssuDate, DriverIsOwner are taken from the body. PlaqueKindId, PlaqueSampleId, VehicleKindId, DriverId, InsuranceCorpId in pasted JSON are ignored.", }) @ApiParam({ name: "client", @@ -178,6 +181,12 @@ export class FanavaranController { name: "claimCaseId", description: "Claim case MongoDB ObjectId", }) + @ApiBody({ + required: false, + description: + "Optional. Paste previous preview JSON if you want to tweak Desc / licence / estimate. Lookup-filter ids in that JSON are ignored.", + schema: { type: "object", additionalProperties: true, example: {} }, + }) async submitDamageCase( @Param("client") client: string, @Param("claimCaseId") claimCaseId: string, @@ -247,7 +256,7 @@ export class FanavaranController { @ApiOperation({ summary: "Preview Fanavaran expertise payload", description: - "Builds the GEN.08 expertise payload from the active damage expert reply, price-drop data, and Fanavaran lookup mappings without calling Fanavaran.", + "GET has no request body. Rebuilds GEN.08 from the active expert reply and lookups on every call. Does not reuse lastPayload cache.", }) @ApiParam({ name: "client", @@ -273,7 +282,7 @@ export class FanavaranController { @ApiOperation({ summary: "Submit Fanavaran expertise request", description: - "Submits the GEN.08 expertise payload for a claim with existing Fanavaran claimId and dmgCaseId. Stores returned Id as local expertiseId.", + "Swagger JSON body is optional. Always rebuilds GEN.08 from the local expert reply and lookups, then POSTs that rebuilt payload. Pasting a previous preview JSON does not skip rebuild. Only money/date fields are taken from the body. DmgCaseId, ClaimExpertId, and DmgSections in pasted JSON are ignored.", }) @ApiParam({ name: "client", @@ -284,6 +293,12 @@ export class FanavaranController { name: "claimCaseId", description: "Claim case MongoDB ObjectId", }) + @ApiBody({ + required: false, + description: + "Optional. Paste previous preview JSON if you want to tweak money/date fields. DmgCaseId / ClaimExpertId / DmgSections in that JSON are ignored.", + schema: { type: "object", additionalProperties: true, example: {} }, + }) async submitExpertise( @Param("client") client: string, @Param("claimCaseId") claimCaseId: string, diff --git a/src/lookups/fanavaran-last-car-policy.spec.ts b/src/lookups/fanavaran-last-car-policy.spec.ts index 424b1ed..cfbc9bf 100644 --- a/src/lookups/fanavaran-last-car-policy.spec.ts +++ b/src/lookups/fanavaran-last-car-policy.spec.ts @@ -5,6 +5,8 @@ import { fanavaranPayloadMatchesDamagedIdentity, fanavaranVehicleMatchesDamagedIdentity, filterPoliciesByLine, + applyFanavaranManualPayloadOverrides, + resolveDamageCasePlaqueLookupIds, insuranceLineLabel, parseLastCarPolicyInput, pickFanavaranPlaqueLookupFields, @@ -465,4 +467,38 @@ describe("fanavaran last car policy", () => { ), ).toBe(true); }); + + it("uses national plaque 8/10 when the damaged VIN is unknown to Fanavaran", () => { + expect(resolveDamageCasePlaqueLookupIds(null)).toEqual({ + kindId: 8, + sampleId: 10, + source: "national-plate-default", + }); + }); + + it("keeps live PlaqueKindId 8 when Swagger body still has stale 15", () => { + expect( + applyFanavaranManualPayloadOverrides( + { + PlaqueKindId: 8, + PlaqueSampleId: 10, + VehicleKindId: 6704, + AccidentVehicleUsedId: 1, + Desc: "سپر جلو", + }, + { + PlaqueKindId: 15, + PlaqueSampleId: 24, + Desc: "سپر جلو (edited)", + }, + ["Desc", "EstimateAmount", "LicenceNo"], + ), + ).toEqual({ + PlaqueKindId: 8, + PlaqueSampleId: 10, + VehicleKindId: 6704, + AccidentVehicleUsedId: 1, + Desc: "سپر جلو (edited)", + }); + }); }); diff --git a/src/lookups/fanavaran-last-car-policy.ts b/src/lookups/fanavaran-last-car-policy.ts index 1a937fa..ce5e1e4 100644 --- a/src/lookups/fanavaran-last-car-policy.ts +++ b/src/lookups/fanavaran-last-car-policy.ts @@ -387,6 +387,89 @@ export function canReuseCachedFanavaranVehicleIds(source: unknown): boolean { return source === "vehicle-inquiry" || source === "vin-inquiry" || source === "vehicle-get"; } +/** Proven Parsian national (چهار تکه) plaque codebook ids. */ +export const FANAVARAN_NATIONAL_PLAQUE_KIND_ID = 8; +export const FANAVARAN_NATIONAL_PLAQUE_SAMPLE_ID = 10; + +export const FANAVARAN_DAMAGE_MANUAL_OVERRIDE_KEYS = [ + "Desc", + "EstimateAmount", + "LicenceNo", + "LicenceIssuDate", + "DriverIsOwner", +] as const; + +export const FANAVARAN_EXPERTISE_MANUAL_OVERRIDE_KEYS = [ + "RepairWage", + "ComponentReplacementCost", + "WasteValue", + "DmgAssessmentDate", + "InspectionTime", + "DamagedVehicleCurrentPrice", + "DropAmountAdditionsDeductions", +] as const; + +export const FANAVARAN_BASE_MANUAL_OVERRIDE_KEYS = [ + "EstimateAmount", + "CulpritLicenceNo", + "CulpritLicenceIssuDate", + "AccidentLocationAddress", + "AccidentDate", + "AnnouncementDate", + "DocReceivedDate", + "AccidentTime", + "PoliceReportDesc", + "PoliceReportSeri", + "PoliceReportSerial", +] as const; + +/** + * Swagger POST body is optional user-entry tweaks only. + * Inquiry / lookup-filter ids always come from the live rebuilt payload. + */ +export function applyFanavaranManualPayloadOverrides( + built: Record, + body: Record | null | undefined, + keys: readonly string[], +): Record { + if (!body) return built; + const next = { ...built }; + for (const key of keys) { + if (!Object.prototype.hasOwnProperty.call(body, key)) continue; + if (body[key] === undefined) continue; + next[key] = body[key]; + } + return next; +} + +/** + * Plaque kind/sample only from a matched Fanavaran vehicle record. + * CII/ESG inquiry, unsourced cache, and tenant defaults of 15 must not win. + * National 4-part plates fall back to 8/10 (lookup-filter safe on Parsian). + */ +export function resolveDamageCasePlaqueLookupIds(matchedVehicle: Record< + string, + unknown +> | null): { + kindId: number; + sampleId: number; + source: "vehicle-inquiry" | "national-plate-default"; +} { + const plaque = pickFanavaranPlaqueLookupFields(matchedVehicle); + if (plaque.kindId != null) { + return { + kindId: plaque.kindId, + sampleId: plaque.sampleId ?? FANAVARAN_NATIONAL_PLAQUE_SAMPLE_ID, + source: "vehicle-inquiry", + }; + } + return { + kindId: FANAVARAN_NATIONAL_PLAQUE_KIND_ID, + sampleId: FANAVARAN_NATIONAL_PLAQUE_SAMPLE_ID, + source: "national-plate-default", + }; +} + /** * Pick the VIN-inquiry / vehicle-GET row that is the damaged car. * Newest VersionNo wins among matches; PlaqueKindId is a tie-break only.