car body implemented

This commit is contained in:
2026-09-16 16:05:54 +03:30
parent a174332d81
commit acb5d2a682
11 changed files with 957 additions and 39 deletions

View File

@@ -49,13 +49,31 @@ import {
pickStoredCarBodyPolicyId,
type FanavaranClaimProduct,
} from "./fanavaran-claim-product";
import {
actualPremiumFromHullPolicyRecord,
customerIdFromHullPolicyRecord,
FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID,
fanavaranHullNestedClaimId,
toFanavaranHullBaseClaimPayload,
} from "./fanavaran-hull-base-claim";
import {
collectFanavaranExpertiseReadinessWarnings,
hullExpertiseAssertFields,
mergeHullVehicleIdentityFromFanavaranVehicle,
pickHullVehicleIdentity,
toFanavaranHullExpertiseDmgSection,
toFanavaranHullExpertisePayload,
vehicleCurrentValueFromHullPolicyRecord,
} from "./fanavaran-hull-expertise";
import {
asHullDmgAccessoryRows,
FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID,
FANAVARAN_DEFAULT_HULL_DMG_KIND_ID,
FANAVARAN_HULL_DMG_COST_KIND_CAPTION,
FANAVARAN_HULL_DMG_KIND_CAPTION,
findLookupIdByCaption,
resolveVehicleHullAccessoryId,
} from "./fanavaran-hull-expertise-lookups";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
import {
@@ -4219,6 +4237,307 @@ export class ClaimRequestManagementService {
}
}
/** GEN.03 hull: TotalPremium + parties inquiry (AccidentCulpritId) before hull field whitelist. */
private async enrichCarBodyHullBaseClaimPayload(input: {
clientKey: FanavaranClientKey;
payload: Record<string, unknown>;
blameCase: { parties?: unknown[] };
/** Blame `Party` row (FIRST party for CAR_BODY); person shape is read via pickPerson* helpers. */
firstParty?: { person?: unknown };
logPrefix: string;
}): Promise<void> {
const policyId = parseFanavaranId(input.payload.PolicyId);
if (policyId == null) return;
let policyRecord: Record<string, unknown> | null = null;
try {
policyRecord = asObjectRecord(
await this.fanavaranLookupService.bodyPolicyById(
input.clientKey,
policyId,
{
contractIdOverride: resolveFanavaranProductContractId(
input.clientKey,
"car-body",
),
},
),
);
} catch (error) {
this.logger.warn(
`${input.logPrefix} hull base claim body policy GET ${policyId} failed: ${
error instanceof Error ? error.message : error
}`,
);
}
const actualPremium = actualPremiumFromHullPolicyRecord(policyRecord);
if (actualPremium != null) {
input.payload.ActualPremium = actualPremium;
}
const person = input.firstParty?.person as
| {
driverIsInsurer?: boolean;
nationalCodeOfDriver?: unknown;
nationalCodeOfInsurer?: unknown;
nationalCode?: unknown;
birthday?: unknown;
driverBirthday?: unknown;
insurerBirthday?: unknown;
}
| undefined;
const nationalCode =
pickPersonNationalCode(person) ??
pickCarBodyPolicyNationalCode(
input.blameCase as { parties?: unknown[] },
);
const birthday = pickPersonBirthday(person);
const driverIsInsurer = person?.driverIsInsurer ?? true;
let accidentCulpritId: number | null = null;
if (nationalCode && birthday) {
accidentCulpritId = await this.resolveDriverFanavaranId(
input.clientKey,
nationalCode,
birthday,
driverIsInsurer,
);
}
if (accidentCulpritId == null) {
accidentCulpritId = customerIdFromHullPolicyRecord(policyRecord);
if (accidentCulpritId != null) {
this.logger.log(
`${input.logPrefix} AccidentCulpritId from policy CustomerId=${accidentCulpritId}`,
);
}
}
if (accidentCulpritId != null) {
input.payload.AccidentCulpritId = accidentCulpritId;
}
const culpritTypeId =
parseFanavaranId(input.payload.CulpritTypeId) ??
FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID;
if (
input.payload.CustomerFaultPercent == null &&
culpritTypeId === 300
) {
input.payload.CustomerFaultPercent = 100;
}
}
/** GEN.06: VehicleCurrentValue from hull policy when price-drop / frontend value is missing. */
private async resolveHullVehicleCurrentValueFromPolicy(
clientKey: FanavaranClientKey,
claimCase: {
fanavaranSync?: {
baseClaim?: { policyId?: unknown; lastPayload?: { PolicyId?: unknown } };
};
},
): Promise<number | null> {
const policyId = parseFanavaranId(
claimCase?.fanavaranSync?.baseClaim?.policyId ??
claimCase?.fanavaranSync?.baseClaim?.lastPayload?.PolicyId,
);
if (policyId == null) return null;
try {
const policy = asObjectRecord(
await this.fanavaranLookupService.bodyPolicyById(
clientKey,
policyId,
{
contractIdOverride: resolveFanavaranProductContractId(
clientKey,
"car-body",
),
},
),
);
return vehicleCurrentValueFromHullPolicyRecord(policy);
} catch (error) {
this.logger.warn(
`[resolveHullVehicleCurrentValueFromPolicy] body policy GET ${policyId} failed: ${
error instanceof Error ? error.message : error
}`,
);
return null;
}
}
private parseHullClaimPolicyId(claimCase: {
fanavaranSync?: {
baseClaim?: { policyId?: unknown; lastPayload?: { PolicyId?: unknown } };
};
}): number | null {
return parseFanavaranId(
claimCase?.fanavaranSync?.baseClaim?.policyId ??
claimCase?.fanavaranSync?.baseClaim?.lastPayload?.PolicyId,
);
}
private async buildCarBodyFanavaranExpertisePayload(input: {
clientKey: FanavaranClientKey;
claimCase: any;
blame: { parties?: unknown[] } | null;
profile: ReturnType<typeof getFanavaranClientProfile>;
parts: any[];
claimExpertId: number;
submittedAt: Date;
priceDropTotal: number;
priceDropCarPrice: unknown;
warnings: string[];
replyKey?: string;
}): Promise<{
payload: Record<string, unknown>;
warnings: string[];
replyKey?: string;
}> {
const policyId = this.parseHullClaimPolicyId(input.claimCase);
const contractOptions = {
contractIdOverride: resolveFanavaranProductContractId(
input.clientKey,
"car-body",
),
};
const [dmgKindRows, dmgCostKindRows, policyRecord, accessoriesRaw] =
await Promise.all([
this.getFanavaranLookupRows(input.clientKey, "vehicle-hull-dmg-kind"),
this.getFanavaranLookupRows(
input.clientKey,
"vehicle-hull-dmg-cost-kinds",
),
policyId != null
? this.fanavaranLookupService.bodyPolicyById(
input.clientKey,
policyId,
contractOptions,
)
: Promise.resolve(null),
policyId != null
? this.fanavaranLookupService.vehicleHullDmgAccessoriesByPolicyId(
input.clientKey,
policyId,
contractOptions,
)
: Promise.resolve([]),
]);
const dmgKindId =
findLookupIdByCaption(dmgKindRows, FANAVARAN_HULL_DMG_KIND_CAPTION) ??
FANAVARAN_DEFAULT_HULL_DMG_KIND_ID;
const dmgCostKindId =
findLookupIdByCaption(
dmgCostKindRows,
FANAVARAN_HULL_DMG_COST_KIND_CAPTION,
) ?? FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID;
const accessories = asHullDmgAccessoryRows(accessoriesRaw);
if (policyId != null && accessories.length === 0) {
input.warnings.push(
`No hull dmg-accessories for policy ${policyId}; VehicleHullAccessoryId may be missing.`,
);
}
let vehicle = pickHullVehicleIdentity(input.blame);
let colorId: number | null = null;
const policy = asObjectRecord(policyRecord);
const vehicleId = pickVehicleId(policy?.VehicleId ?? policy?.vehicleId);
const versionNo = parseFanavaranId(policy?.VehicleVersionNo);
if (vehicleId != null) {
try {
const vehicleRow = asObjectRecord(
await this.fanavaranLookupService.vehicleById(
input.clientKey,
vehicleId,
versionNo ?? undefined,
contractOptions,
),
);
vehicle = mergeHullVehicleIdentityFromFanavaranVehicle(
vehicle,
vehicleRow,
);
colorId = parseFanavaranId(vehicleRow.ColorId);
} catch (error) {
this.logger.warn(
`[buildCarBodyFanavaranExpertisePayload] vehicle GET ${vehicleId} failed: ${
error instanceof Error ? error.message : error
}`,
);
}
}
const wage = input.parts.reduce(
(sum, part) => sum + this.parseFanavaranMoney(part?.salary),
0,
);
const componentReplacementCost = input.parts.reduce(
(sum, part) => sum + this.parseFanavaranMoney(part?.price),
0,
);
const wasteValue = input.parts.reduce(
(sum, part) => sum + this.fanavaranDaghiWasteValue(part?.daghi),
0,
);
const hullSections = input.parts.map((part: any) => {
const accessoryKindId = parseCatalogPartIdInput(part?.partId);
const vehicleHullAccessoryId = resolveVehicleHullAccessoryId(
accessories,
accessoryKindId,
);
if (vehicleHullAccessoryId == null) {
input.warnings.push(
`No VehicleHullAccessoryId for part "${this.fanavaranPartLabel(part)}" (AccessoryKindId=${accessoryKindId ?? "none"}).`,
);
}
return toFanavaranHullExpertiseDmgSection({
partId: part?.partId,
desc: this.fanavaranPartLabel(part),
wasteValue: this.fanavaranDaghiWasteValue(part?.daghi),
amount:
this.parseFanavaranMoney(part?.price) +
this.parseFanavaranMoney(part?.salary),
dmgKindId,
dmgCostKindId,
vehicleHullAccessoryId,
});
});
let vehicleCurrentValue =
this.parseFanavaranMoney(input.priceDropCarPrice) || null;
if (vehicleCurrentValue == null) {
vehicleCurrentValue = vehicleCurrentValueFromHullPolicyRecord(policy);
}
const repairDuration =
typeof input.profile.defaults.HullExpertiseRepairDuration === "number"
? input.profile.defaults.HullExpertiseRepairDuration
: null;
return {
replyKey: input.replyKey,
warnings: input.warnings,
payload: toFanavaranHullExpertisePayload({
claimExpertId: input.claimExpertId,
dmgAssessmentDate: this.convertToPersianDate(input.submittedAt),
inspectionTime: this.getTime24Hour(input.submittedAt),
wage,
componentReplacementCost,
wasteValue,
dropAmount: input.priceDropTotal || 0,
vehicleCurrentValue,
vehicle,
colorId,
repairDuration,
dmgSections: hullSections,
}),
};
}
private async buildFanavaranDamageCasePayload(input: {
claimCase: any;
blameCase?: any;
@@ -5743,6 +6062,19 @@ export class ClaimRequestManagementService {
);
}
if (fanavaranClaimProductFromBlameType(blameCase.type) === "car-body") {
await this.enrichCarBodyHullBaseClaimPayload({
clientKey,
payload,
blameCase,
firstParty,
logPrefix,
});
const hull = toFanavaranHullBaseClaimPayload(payload);
for (const key of Object.keys(payload)) delete payload[key];
Object.assign(payload, hull);
}
if (persistPayload) {
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
$set: {
@@ -6153,7 +6485,34 @@ export class ClaimRequestManagementService {
};
}
const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/files`;
await this.syncFanavaranHullBaseClaim(claimCaseId, clientKey);
claimCase = await this.claimCaseDbService.findById(claimCaseId);
const nestedClaimId = fanavaranHullNestedClaimId({
claimId: claimCase?.claimId,
claimNo: claimCase?.claimNo,
});
if (nestedClaimId == null) {
return {
clientKey,
claimCaseId,
totalLocalImages: candidates.length,
skippedAlreadySubmitted: candidates.length - pending.length,
attempted: 0,
submitted: 0,
failed: 0,
skipped: pending.length,
warning: "Fanavaran base claimId is missing",
results: pending.map((c) => ({
attempted: false,
submitted: false,
skipped: true,
skipReason: "Fanavaran base claimId is missing",
fileName: c.fileName,
})),
};
}
const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${nestedClaimId}/files`;
const fileTypeId = profile.defaults.ClaimFileTypeId;
const product = await this.resolveFanavaranClaimProduct(claimCaseId);
@@ -6166,7 +6525,7 @@ export class ClaimRequestManagementService {
const content = {
FileName: candidate.fileName,
FileTypeId: fileTypeId,
...(product === "car-body" ? { ClaimId: claimCase.claimId } : {}),
...(product === "car-body" ? { ClaimId: nestedClaimId } : {}),
};
this.logger.log(
@@ -6188,7 +6547,7 @@ export class ClaimRequestManagementService {
results.push({
attempted: true,
submitted: true,
claimId: claimCase.claimId,
claimId: nestedClaimId,
fileName: candidate.fileName,
submitUrl: url,
fanavaranResponse: response.data,
@@ -6203,7 +6562,7 @@ export class ClaimRequestManagementService {
submitted: false,
warning,
fileName: candidate.fileName,
claimId: claimCase.claimId,
claimId: nestedClaimId,
submitUrl: url,
});
@@ -6752,33 +7111,19 @@ export class ClaimRequestManagementService {
String(input.claimCase.blameRequestId),
)
: null;
const hullSections = parts.map((part: any) =>
toFanavaranHullExpertiseDmgSection({
partId: part?.partId,
desc: this.fanavaranPartLabel(part),
wasteValue: this.fanavaranDaghiWasteValue(part?.daghi),
amount:
this.parseFanavaranMoney(part?.price) +
this.parseFanavaranMoney(part?.salary),
}),
);
return {
replyKey: active?.replyKey,
return this.buildCarBodyFanavaranExpertisePayload({
clientKey: input.clientKey,
claimCase: input.claimCase,
blame,
profile,
parts,
claimExpertId,
submittedAt,
priceDropTotal,
priceDropCarPrice: priceDrop?.carPrice,
warnings,
payload: toFanavaranHullExpertisePayload({
claimExpertId,
dmgAssessmentDate: this.convertToPersianDate(submittedAt),
inspectionTime: this.getTime24Hour(submittedAt),
wage: repairWage,
componentReplacementCost,
wasteValue,
dropAmount: priceDropTotal || 0,
vehicleCurrentValue:
this.parseFanavaranMoney(priceDrop?.carPrice) || null,
vehicle: pickHullVehicleIdentity(blame),
dmgSections: hullSections,
}),
};
replyKey: active?.replyKey,
});
}
const payload: Record<string, unknown> = {
@@ -6940,7 +7285,18 @@ export class ClaimRequestManagementService {
}
);
}
const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/expertise`;
await this.syncFanavaranHullBaseClaim(claimCaseId, clientKey);
claimCase = await this.claimCaseDbService.findById(claimCaseId);
const nestedClaimId = fanavaranHullNestedClaimId({
claimId: claimCase?.claimId,
claimNo: claimCase?.claimNo,
});
if (nestedClaimId == null) {
throw new BadRequestException(
"Fanavaran claimId is required before submitting expertise",
);
}
const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${nestedClaimId}/expertise`;
const startedAt = Date.now();
const auditSession: FanavaranAuditSession = {
trackingCode: this.fanavaranAuditService.generateTrackingCode(),
@@ -7332,6 +7688,70 @@ export class ClaimRequestManagementService {
}
}
private async putFanavaranJson(
url: string,
payload: Record<string, unknown>,
clientKey: FanavaranClientKey,
auditSession?: FanavaranAuditSession,
claimCaseId?: string,
) {
this.fanavaranAuthService.assertNotInBackoff(clientKey);
const headers = await this.getFanavaranAuthHeaders(
clientKey,
auditSession,
claimCaseId,
);
try {
const response = await firstValueFrom(
this.httpService.put(url, payload, {
headers: {
...headers,
"Content-Type": "application/json",
},
}),
);
this.fanavaranAuthService.clearBackoff(clientKey);
return response;
} catch (error) {
this.fanavaranAuthService.registerFailure(clientKey, error);
throw error;
}
}
/**
* Nested hull files/expertise look up a بدنه پرونده. Create often echoed a ثالث-shaped
* body; PUT the GEN.03 hull shape onto the existing Id before follow-up stages.
*/
private async syncFanavaranHullBaseClaim(
claimCaseId: string,
clientKey: FanavaranClientKey,
): Promise<void> {
const product = await this.resolveFanavaranClaimProduct(claimCaseId);
if (product !== "car-body") return;
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
const nestedId = fanavaranHullNestedClaimId({
claimId: claimCase?.claimId,
claimNo: claimCase?.claimNo,
});
if (nestedId == null) return;
const built = await this.previewFanavaranSubmitV2(claimCaseId, clientKey, {
requirePolicyId: true,
persistPayload: true,
});
const hull = toFanavaranHullBaseClaimPayload(
built && typeof built === "object" && "payload" in built
? (built as { payload: Record<string, unknown> }).payload
: built,
);
const url = `${fanavaranClaimsBaseUrl("car-body")}/${nestedId}`;
this.logger.log(
`[Fanavaran hull sync] PUT ${url} keys=${Object.keys(hull).join(",")}`,
);
await this.putFanavaranJson(url, hull, clientKey, undefined, claimCaseId);
}
private async postFanavaranMultipart(
url: string,
content: Record<string, unknown>,