Merge pull request 'third party update , optional body is done' (#320) from s.hajizadeh/yara724api:main into main

Reviewed-on: Yara724/api#320
This commit is contained in:
2026-09-15 16:36:52 +03:30
6 changed files with 386 additions and 182 deletions

View File

@@ -37,8 +37,11 @@ import { ClaimRequestManagementDbService } from "./entites/db-service/claim-requ
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service"; import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
import { fanavaranClaimReferences } from "./fanavaran-claim-references"; import { fanavaranClaimReferences } from "./fanavaran-claim-references";
import { import {
FANAVARAN_HULL_NO_DAMAGE_CASE_REASON,
fanavaranClaimProductFromBlameType, fanavaranClaimProductFromBlameType,
fanavaranClaimsBaseUrl, fanavaranClaimsBaseUrl,
fanavaranClaimStageUrl,
fanavaranHasDamageCaseStage,
fanavaranInsuranceLineIdForProduct, fanavaranInsuranceLineIdForProduct,
fanavaranPartyInquirySources, fanavaranPartyInquirySources,
isFanavaranSubmitSupportedBlameType, isFanavaranSubmitSupportedBlameType,
@@ -228,18 +231,20 @@ import {
import { import {
asObjectRecord, asObjectRecord,
asVehicleInquiryRows, asVehicleInquiryRows,
canReuseCachedFanavaranVehicleIds,
collectPartyVinCandidates, collectPartyVinCandidates,
completePlaqueParts, completePlaqueParts,
fanavaranIdOrConfigDefault,
fanavaranPayloadMatchesDamagedIdentity,
fanavaranVehicleMatchesDamagedIdentity, fanavaranVehicleMatchesDamagedIdentity,
normalizeVin, normalizeVin,
applyFanavaranManualPayloadOverrides,
FANAVARAN_BASE_MANUAL_OVERRIDE_KEYS,
FANAVARAN_DAMAGE_MANUAL_OVERRIDE_KEYS,
FANAVARAN_EXPERTISE_MANUAL_OVERRIDE_KEYS,
parseFanavaranId, parseFanavaranId,
pickFanavaranPlaqueLookupFields, pickFanavaranPlaqueLookupFields,
pickFanavaranVehicleFromInquiry, pickFanavaranVehicleFromInquiry,
pickVehicleId, pickVehicleId,
pickVinFromPartyVehicle, pickVinFromPartyVehicle,
resolveDamageCasePlaqueLookupIds,
selectFanavaranVehicleForDamageCase, selectFanavaranVehicleForDamageCase,
type FanavaranDamagedVehicleIdentity, type FanavaranDamagedVehicleIdentity,
} from "src/lookups/fanavaran-last-car-policy"; } from "src/lookups/fanavaran-last-car-policy";
@@ -4336,17 +4341,6 @@ export class ClaimRequestManagementService {
} }
const cachedDamage = const cachedDamage =
(input.claimCase as any)?.fanavaranSync?.damageCase ?? {}; (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( 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"}`, `[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, product,
); );
const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle); const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle);
const inquiryVehicle = const plaqueLookup = resolveDamageCasePlaqueLookupIds(apiVehicle);
asObjectRecord(inquiryMapped) ?? asObjectRecord(inquiryRaw); const plaqueKindId = plaqueLookup.kindId;
const inquiryPlaque = fanavaranVehicleMatchesDamagedIdentity( const plaqueSampleId = plaqueLookup.sampleId;
inquiryVehicle, const plaqueKindSource = plaqueLookup.source;
damagedIdentity, const plaqueSampleSource = plaqueLookup.source;
) if (plaqueLookup.source !== "vehicle-inquiry") {
? 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) {
this.logger.log( this.logger.log(
`[buildFanavaranDamageCasePayload] PlaqueKindId API miss; using config default ${plaqueKindId}`, `[buildFanavaranDamageCasePayload] PlaqueKindId not on a matched Fanavaran vehicle; using national-plate default ${plaqueKindId}/${plaqueSampleId}`,
);
}
if (plaqueSampleFromApi == null) {
this.logger.log(
`[buildFanavaranDamageCasePayload] PlaqueSampleId API miss; using config default ${plaqueSampleId}`,
); );
} }
const builtYear = const builtYear =
parseFanavaranId(apiVehicle?.BuiltYear) ?? builtYearFromInquiry; parseFanavaranId(apiVehicle?.BuiltYear) ?? builtYearFromInquiry;
const carType = input.claimCase?.vehicle?.carType as string | undefined; const carType = input.claimCase?.vehicle?.carType as string | undefined;
let vehicleKindId = this.firstPositiveFanavaranId( let vehicleKindId = this.firstPositiveFanavaranId(apiVehicle?.VehicleKindId);
apiVehicle?.VehicleKindId,
canReuseCachedVehicleKind ? cachedDamage.vehicleKindId : null,
canReuseCachedVehicleKind ? lastDamagePayload?.VehicleKindId : null,
);
let vehicleKindSource: "vehicle-inquiry" | "car-type-lookup" | null = let vehicleKindSource: "vehicle-inquiry" | "car-type-lookup" | null =
parseFanavaranId(apiVehicle?.VehicleKindId) != null parseFanavaranId(apiVehicle?.VehicleKindId) != null
? "vehicle-inquiry" ? "vehicle-inquiry"
: canReuseCachedVehicleKind : null;
? "vehicle-inquiry"
: null;
if (vehicleKindId == null) { if (vehicleKindId == null) {
vehicleKindId = await this.resolveVehicleKindId( vehicleKindId = await this.resolveVehicleKindId(
input.clientKey, input.clientKey,
@@ -4423,84 +4379,62 @@ export class ClaimRequestManagementService {
if (vehicleKindId != null) vehicleKindSource = "car-type-lookup"; if (vehicleKindId != null) vehicleKindSource = "car-type-lookup";
} }
// Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload let driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase(
let driverFanavaranId = this.firstPositiveFanavaranId( input.clientKey,
cachedDamage.driverId, input.blameCase?.parties ?? [],
cachedDamage.otherPersonId, person,
person.fanavaranDriverId, input.claimCase,
lastDamagePayload?.DriverId,
); );
let otherPersonId = this.firstPositiveFanavaranId(cachedDamage.otherPersonId); let otherPersonId = this.firstPositiveFanavaranId(cachedDamage.otherPersonId);
if (driverFanavaranId) { if (driverFanavaranId) {
this.logger.log( this.logger.log(
`[buildFanavaranDamageCasePayload] Using CACHED DriverId=${driverFanavaranId}`, `[buildFanavaranDamageCasePayload] DriverId=${driverFanavaranId} from live parties inquiry`,
); );
} else { } else if (otherPersonId) {
driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase( driverFanavaranId = otherPersonId;
input.clientKey, this.logger.log(
input.blameCase?.parties ?? [], `[buildFanavaranDamageCasePayload] Parties inquiry missed; reusing GEN.44 otherPersonId=${otherPersonId}`,
);
} else if (input.registerMissingPerson) {
const registered = await this.registerFanavaranOtherPerson({
clientKey: input.clientKey,
person, person,
input.claimCase, claimCaseId: input.claimCase?._id
); ? String(input.claimCase._id)
: undefined,
if (!driverFanavaranId && input.registerMissingPerson) { });
const registered = await this.registerFanavaranOtherPerson({ if (registered != null) {
clientKey: input.clientKey, driverFanavaranId = registered;
person, otherPersonId = registered;
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}]`,
);
}
} }
} }
let insuranceCorpId = if (driverFanavaranId && input.blameCase?._id && damagedParty) {
cachedDamage.insuranceCorpId != null const partyIndex = (input.blameCase.parties ?? []).findIndex(
? Number(cachedDamage.insuranceCorpId) (p: any) => p === damagedParty ||
: null; String(p?.person?.userId ?? "") ===
if (insuranceCorpId == null) { String(damagedParty?.person?.userId ?? ""),
insuranceCorpId = await this.fanavaranLookupService.resolveInsuranceCorpId( );
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, input.clientKey,
); );
}
const cachedUsedId = let accidentVehicleUsedId: number | null = null;
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 usedSource: "vin-inquiry" | "policy-vehicle" | "vehicle-inquiry" | null = let usedSource: "vin-inquiry" | "policy-vehicle" | "vehicle-inquiry" | null =
canReuseApiUsedId ? "vehicle-inquiry" : null; null;
if (accidentVehicleUsedId == null && apiVehicle) { if (accidentVehicleUsedId == null && apiVehicle) {
const fromDamagedVehicle = const fromDamagedVehicle =
await this.resolveAccidentVehicleUsedFromVehicleRecord( await this.resolveAccidentVehicleUsedFromVehicleRecord(
@@ -4597,10 +4531,7 @@ export class ClaimRequestManagementService {
PolicyCINumber: policyCINumber, PolicyCINumber: policyCINumber,
}; };
const persistedPayload = this.keepPreviousPositiveIds(lastDamagePayload, payload, [ const persistedPayload = payload;
"DriverId",
"InsuranceCorpId",
]);
// Persist Fanavaran-sourced IDs + last payload for reuse on later preview/submit // Persist Fanavaran-sourced IDs + last payload for reuse on later preview/submit
if (input.claimCase?._id) { if (input.claimCase?._id) {
@@ -5502,12 +5433,9 @@ export class ClaimRequestManagementService {
/** /**
* Build Fanavaran GEN.03 payload from local claim/blame data. * Build Fanavaran GEN.03 payload from local claim/blame data.
* *
* Fanavaran-sourced fields (PolicyId) use resolve-once caching: * HTTP preview/submit always pass forceRefreshPolicy + forceRefreshUsedId,
* - If `fanavaranSync.baseClaim.policyId` exists → reuse (no policy HTTP). * so PolicyId and AccidentVehicleUsedId are re-inquired every call.
* - Else → live inquiry-my-policies once, then persist policyId + lastPayload.
*
* Auth still uses the shared midnight-TTL token cache (not a new login per preview). * 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( async previewFanavaranSubmitV2(
claimCaseId: string, claimCaseId: string,
@@ -5577,7 +5505,7 @@ export class ClaimRequestManagementService {
}, },
failureReason: null as string | null, failureReason: null as string | null,
note: 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); 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({ const payload = await this.buildFanavaranDamageCasePayload({
claimCase, claimCase,
blameCase, blameCase,
@@ -5935,7 +5878,7 @@ export class ClaimRequestManagementService {
claimId: claimCase.claimId ?? null, claimId: claimCase.claimId ?? null,
dmgCaseId: claimCase.dmgCaseId ?? null, dmgCaseId: claimCase.dmgCaseId ?? null,
submitUrl: claimCase.claimId submitUrl: claimCase.claimId
? `${fanavaranClaimsBaseUrl(fanavaranClaimProductFromBlameType(blameCase.type))}/${claimCase.claimId}/dmg-cases` ? fanavaranClaimStageUrl(product, "damage-case", claimCase.claimId)
: null, : null,
warning: warnings.length ? warnings.join(" ") : undefined, warning: warnings.length ? warnings.join(" ") : undefined,
payload, payload,
@@ -6775,15 +6718,14 @@ export class ClaimRequestManagementService {
0, 0,
); );
const submittedAt = reply?.submittedAt ?? new Date(); const submittedAt = reply?.submittedAt ?? new Date();
const product = await this.resolveFanavaranClaimProduct(
input.claimCase?._id ? String(input.claimCase._id) : undefined,
);
return { const payload: Record<string, unknown> = {
replyKey: active?.replyKey,
warnings,
payload: {
ClaimExpertId: claimExpertId, ClaimExpertId: claimExpertId,
ComponentReplacementCost: componentReplacementCost, ComponentReplacementCost: componentReplacementCost,
DmgAssessmentDate: this.convertToPersianDate(submittedAt), DmgAssessmentDate: this.convertToPersianDate(submittedAt),
DmgCaseId: input.claimCase.dmgCaseId ?? null,
InspectionPlaceId: inspectionPlaceId, InspectionPlaceId: inspectionPlaceId,
InspectionTime: this.getTime24Hour(submittedAt), InspectionTime: this.getTime24Hour(submittedAt),
RepairWage: repairWage, RepairWage: repairWage,
@@ -6795,18 +6737,28 @@ export class ClaimRequestManagementService {
DamagedVehicleCurrentPrice: DamagedVehicleCurrentPrice:
this.parseFanavaranMoney(priceDrop?.carPrice) || null, this.parseFanavaranMoney(priceDrop?.carPrice) || null,
DmgSections: dmgSections, DmgSections: dmgSections,
}, };
if (fanavaranHasDamageCaseStage(product)) {
payload.DmgCaseId = input.claimCase.dmgCaseId ?? null;
}
return {
replyKey: active?.replyKey,
warnings,
payload,
}; };
} }
private assertFanavaranExpertisePayloadReady( private assertFanavaranExpertisePayloadReady(
payload: Record<string, unknown>, payload: Record<string, unknown>,
warnings: string[], warnings: string[],
options?: { requireDmgCaseId?: boolean },
): void { ): void {
const sections = Array.isArray(payload.DmgSections) const sections = Array.isArray(payload.DmgSections)
? 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) if (!payload.InspectionPlaceId)
warnings.push("InspectionPlaceId is required."); warnings.push("InspectionPlaceId is required.");
if (!payload.DropAmountStatus) if (!payload.DropAmountStatus)
@@ -6876,17 +6828,19 @@ export class ClaimRequestManagementService {
); );
const claimCase = await this.claimCaseDbService.findById(claimCaseId); const claimCase = await this.claimCaseDbService.findById(claimCaseId);
if (!claimCase) throw new NotFoundException("Claim case not found"); if (!claimCase) throw new NotFoundException("Claim case not found");
let payload: Record<string, unknown>; const product = await this.resolveFanavaranClaimProduct(claimCaseId);
if (bodyOverride) { const built = await this.buildFanavaranExpertisePayload({
payload = bodyOverride; claimCase,
} else { clientKey,
const built = await this.buildFanavaranExpertisePayload({ });
claimCase, const payload = applyFanavaranManualPayloadOverrides(
clientKey, built.payload,
}); bodyOverride,
payload = built.payload; FANAVARAN_EXPERTISE_MANUAL_OVERRIDE_KEYS,
this.assertFanavaranExpertisePayloadReady(payload, built.warnings); );
} this.assertFanavaranExpertisePayloadReady(payload, built.warnings, {
requireDmgCaseId: fanavaranHasDamageCaseStage(product),
});
return await this.executeFanavaranExpertiseSubmit( return await this.executeFanavaranExpertiseSubmit(
claimCaseId, claimCaseId,
clientKey, clientKey,
@@ -6910,7 +6864,11 @@ export class ClaimRequestManagementService {
auditSource: FanavaranAuditSource, auditSource: FanavaranAuditSource,
): Promise<any> { ): Promise<any> {
let claimCase = await this.claimCaseDbService.findById(claimCaseId); 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( await this.ensureFanavaranDamageCase(
claimCaseId, claimCaseId,
clientKey, clientKey,
@@ -7063,7 +7021,8 @@ export class ClaimRequestManagementService {
await this.autoSubmitToFanavaranV2OnClaimCreated(claimCaseId); await this.autoSubmitToFanavaranV2OnClaimCreated(claimCaseId);
claimCase = await this.claimCaseDbService.findById(claimCaseId); claimCase = await this.claimCaseDbService.findById(claimCaseId);
} }
if (!claimCase?.dmgCaseId) { const product = await this.resolveFanavaranClaimProduct(claimCaseId);
if (fanavaranHasDamageCaseStage(product) && !claimCase?.dmgCaseId) {
await this.autoSubmitFanavaranDamageCaseOnOuterPartsSelected( await this.autoSubmitFanavaranDamageCaseOnOuterPartsSelected(
claimCaseId, claimCaseId,
claimCase?.damage?.selectedParts ?? [], claimCase?.damage?.selectedParts ?? [],
@@ -7075,7 +7034,9 @@ export class ClaimRequestManagementService {
claimCase, claimCase,
clientKey, clientKey,
}); });
this.assertFanavaranExpertisePayloadReady(payload, warnings); this.assertFanavaranExpertisePayloadReady(payload, warnings, {
requireDmgCaseId: fanavaranHasDamageCaseStage(product),
});
const fanavaranResponse = await this.executeFanavaranExpertiseSubmit( const fanavaranResponse = await this.executeFanavaranExpertiseSubmit(
claimCaseId, claimCaseId,
@@ -7245,7 +7206,11 @@ export class ClaimRequestManagementService {
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
auditSource: FanavaranAuditSource, auditSource: FanavaranAuditSource,
selectedParts?: unknown[], selectedParts?: unknown[],
): Promise<{ claimId: number; dmgCaseId: number; created: boolean }> { ): Promise<{
claimId: number;
dmgCaseId: number | null;
created: boolean;
}> {
await this.ensureFanavaranBaseClaim(claimCaseId, clientKey, auditSource); await this.ensureFanavaranBaseClaim(claimCaseId, clientKey, auditSource);
const existing = await this.claimCaseDbService.findById(claimCaseId); 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( this.logger.log(
`[Fanavaran ${clientKey}] Soft-ensuring damage case before next stage claimCaseId=${claimCaseId}`, `[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( this.logger.log(
`[executeFanavaranDamageCaseSubmit] Already have dmgCaseId=${claimCase.dmgCaseId}; skipping Fanavaran POST`, `[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, claimCase,
blameCase, blameCase,
selectedParts: selectedParts ?? claimCase.damage?.selectedParts ?? [], selectedParts: selectedParts ?? claimCase.damage?.selectedParts ?? [],
@@ -7430,11 +7436,15 @@ export class ClaimRequestManagementService {
defaults: profile.defaults, defaults: profile.defaults,
registerMissingPerson: true, registerMissingPerson: true,
}); });
const payload = applyFanavaranManualPayloadOverrides(
builtPayload,
bodyOverride,
FANAVARAN_DAMAGE_MANUAL_OVERRIDE_KEYS,
);
// Guardrail: never send empty/null licence fields even if manual overrides arrive. // Guardrail: never send empty/null licence fields even if manual overrides arrive.
payload.LicenceNo = this.ensureFanavaranNonEmptyLicenceNo(payload.LicenceNo); payload.LicenceNo = this.ensureFanavaranNonEmptyLicenceNo(payload.LicenceNo);
const url = `${fanavaranClaimsBaseUrl(fanavaranClaimProductFromBlameType(blameCase.type))}/${claimCase.claimId}/dmg-cases`;
this.logger.log( this.logger.log(
`[executeFanavaranDamageCaseSubmit] claimCaseId=${claimCaseId} claimId=${claimCase.claimId} payload.DriverId=${payload.DriverId ?? "NULL"} payload keys=${Object.keys(payload).join(", ")}`, `[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, clientKey,
selectedParts, 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; const dmgCaseId = fanavaranResponse?.Id;
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
@@ -8208,7 +8228,7 @@ export class ClaimRequestManagementService {
); );
const existing = await this.claimCaseDbService.findById(claimCaseId); const existing = await this.claimCaseDbService.findById(claimCaseId);
if (existing?.claimId != null && !bodyOverride) { if (existing?.claimId != null) {
this.logger.log( this.logger.log(
`${logPrefix} Base claim already exists claimId=${existing.claimId}; skipping Fanavaran POST`, `${logPrefix} Base claim already exists claimId=${existing.claimId}; skipping Fanavaran POST`,
); );
@@ -8227,14 +8247,19 @@ export class ClaimRequestManagementService {
claimCaseId, claimCaseId,
}; };
const fanavaranData = const built =
bodyOverride ?? await this.previewFanavaranSubmitV2(claimCaseId, clientKey, {
(await this.previewFanavaranSubmitV2(claimCaseId, clientKey, {
requirePolicyId: true, requirePolicyId: true,
auditSession, auditSession,
persistPayload: true, persistPayload: true,
forceRefreshPolicy: true,
forceRefreshUsedId: 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. // Guardrail: never send empty/null licence fields even if manual overrides arrive.
fanavaranData.CulpritLicenceNo = this.ensureFanavaranNonEmptyLicenceNo( fanavaranData.CulpritLicenceNo = this.ensureFanavaranNonEmptyLicenceNo(

View File

@@ -3,6 +3,8 @@ import { PartyRole } from "src/request-management/entities/schema/partyRole.enum
import { import {
fanavaranClaimProductFromBlameType, fanavaranClaimProductFromBlameType,
fanavaranClaimsBaseUrl, fanavaranClaimsBaseUrl,
fanavaranClaimStageUrl,
fanavaranHasDamageCaseStage,
fanavaranPartyInquirySources, fanavaranPartyInquirySources,
isFanavaranSubmitSupportedBlameType, isFanavaranSubmitSupportedBlameType,
pickCarBodyPolicyNationalCode, 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", () => { it("allows Fanavaran submit for both car products", () => {
expect(isFanavaranSubmitSupportedBlameType("THIRD_PARTY")).toBe(true); expect(isFanavaranSubmitSupportedBlameType("THIRD_PARTY")).toBe(true);
expect(isFanavaranSubmitSupportedBlameType("CAR_BODY")).toBe(true); expect(isFanavaranSubmitSupportedBlameType("CAR_BODY")).toBe(true);

View File

@@ -46,6 +46,35 @@ export function fanavaranClaimsBaseUrl(
return `${FANAVARAN_CLAIMS_HOST}/${fanavaranClaimsResourcePath(product)}`; 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( export function fanavaranInsuranceLineIdForProduct(
product: FanavaranClaimProduct, product: FanavaranClaimProduct,
): number { ): number {

View File

@@ -10,6 +10,7 @@ import {
} from "@nestjs/common"; } from "@nestjs/common";
import { import {
ApiBearerAuth, ApiBearerAuth,
ApiBody,
ApiOperation, ApiOperation,
ApiParam, ApiParam,
ApiQuery, ApiQuery,
@@ -57,7 +58,7 @@ export class FanavaranController {
@ApiOperation({ @ApiOperation({
summary: "Preview Fanavaran base claim create payload", summary: "Preview Fanavaran base claim create payload",
description: 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({ @ApiParam({
name: "client", name: "client",
@@ -76,34 +77,30 @@ export class FanavaranController {
@ApiQuery({ @ApiQuery({
name: "forceRefreshPolicy", name: "forceRefreshPolicy",
required: false, required: false,
description: deprecated: true,
"When true, ignores cached PolicyId and performs a live Fanavaran policy inquiry again. Do not pass this from normal UI loads.", description: "Ignored. Preview always re-inquires PolicyId.",
}) })
@ApiQuery({ @ApiQuery({
name: "resolvePolicy", name: "resolvePolicy",
required: false, required: false,
deprecated: true, deprecated: true,
description: description: "Ignored. Preview always re-inquires PolicyId.",
"Deprecated. Ignored for cache-busting. PolicyId is resolve-once from fanavaranSync.baseClaim.policyId; use forceRefreshPolicy=true only to re-inquire.",
}) })
async preview( async preview(
@Param("client") client: string, @Param("client") client: string,
@Param("claimCaseId") claimCaseId: string, @Param("claimCaseId") claimCaseId: string,
@Query("debug") debug?: string, @Query("debug") debug?: string,
@Query("forceRefreshPolicy") forceRefreshPolicy?: string, @Query("forceRefreshPolicy") _forceRefreshPolicy?: string,
@Query("resolvePolicy") _resolvePolicy?: string, @Query("resolvePolicy") _resolvePolicy?: string,
) { ) {
const clientKey = this.parseClientParam(client); 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( return await this.claimRequestManagementService.previewFanavaranSubmitV2(
claimCaseId, claimCaseId,
clientKey, clientKey,
{ {
debug: debug === "1" || debug === "true", debug: debug === "1" || debug === "true",
forceRefreshPolicy: forceRefreshPolicy: true,
forceRefreshPolicy === "1" || forceRefreshPolicy === "true", forceRefreshUsedId: true,
requirePolicyId: false, requirePolicyId: false,
}, },
); );
@@ -113,7 +110,7 @@ export class FanavaranController {
@ApiOperation({ @ApiOperation({
summary: "Submit Fanavaran base claim create request", summary: "Submit Fanavaran base claim create request",
description: 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({ @ApiParam({
name: "client", name: "client",
@@ -124,6 +121,12 @@ export class FanavaranController {
name: "claimCaseId", name: "claimCaseId",
description: "Claim case MongoDB ObjectId", 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( async submit(
@Param("client") client: string, @Param("client") client: string,
@Param("claimCaseId") claimCaseId: string, @Param("claimCaseId") claimCaseId: string,
@@ -141,7 +144,7 @@ export class FanavaranController {
@ApiOperation({ @ApiOperation({
summary: "Preview Fanavaran damage-case payload", summary: "Preview Fanavaran damage-case payload",
description: 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({ @ApiParam({
name: "client", name: "client",
@@ -167,7 +170,7 @@ export class FanavaranController {
@ApiOperation({ @ApiOperation({
summary: "Submit Fanavaran damage-case request", summary: "Submit Fanavaran damage-case request",
description: 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({ @ApiParam({
name: "client", name: "client",
@@ -178,6 +181,12 @@ export class FanavaranController {
name: "claimCaseId", name: "claimCaseId",
description: "Claim case MongoDB ObjectId", 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( async submitDamageCase(
@Param("client") client: string, @Param("client") client: string,
@Param("claimCaseId") claimCaseId: string, @Param("claimCaseId") claimCaseId: string,
@@ -247,7 +256,7 @@ export class FanavaranController {
@ApiOperation({ @ApiOperation({
summary: "Preview Fanavaran expertise payload", summary: "Preview Fanavaran expertise payload",
description: 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({ @ApiParam({
name: "client", name: "client",
@@ -273,7 +282,7 @@ export class FanavaranController {
@ApiOperation({ @ApiOperation({
summary: "Submit Fanavaran expertise request", summary: "Submit Fanavaran expertise request",
description: 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({ @ApiParam({
name: "client", name: "client",
@@ -284,6 +293,12 @@ export class FanavaranController {
name: "claimCaseId", name: "claimCaseId",
description: "Claim case MongoDB ObjectId", 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( async submitExpertise(
@Param("client") client: string, @Param("client") client: string,
@Param("claimCaseId") claimCaseId: string, @Param("claimCaseId") claimCaseId: string,

View File

@@ -5,6 +5,8 @@ import {
fanavaranPayloadMatchesDamagedIdentity, fanavaranPayloadMatchesDamagedIdentity,
fanavaranVehicleMatchesDamagedIdentity, fanavaranVehicleMatchesDamagedIdentity,
filterPoliciesByLine, filterPoliciesByLine,
applyFanavaranManualPayloadOverrides,
resolveDamageCasePlaqueLookupIds,
insuranceLineLabel, insuranceLineLabel,
parseLastCarPolicyInput, parseLastCarPolicyInput,
pickFanavaranPlaqueLookupFields, pickFanavaranPlaqueLookupFields,
@@ -465,4 +467,38 @@ describe("fanavaran last car policy", () => {
), ),
).toBe(true); ).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)",
});
});
}); });

View File

@@ -387,6 +387,89 @@ export function canReuseCachedFanavaranVehicleIds(source: unknown): boolean {
return source === "vehicle-inquiry" || source === "vin-inquiry" || source === "vehicle-get"; 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<string, unknown>,
body: Record<string, unknown> | null | undefined,
keys: readonly string[],
): Record<string, unknown> {
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. * Pick the VIN-inquiry / vehicle-GET row that is the damaged car.
* Newest VersionNo wins among matches; PlaqueKindId is a tie-break only. * Newest VersionNo wins among matches; PlaqueKindId is a tie-break only.