third party update , optional body is done

This commit is contained in:
2026-09-15 16:36:13 +03:30
parent 073b4dec38
commit 0b0a1dfa13
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 { 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<string, unknown> = {
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<string, unknown>,
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<string, unknown>;
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<any> {
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(