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, pickStoredCarBodyPolicyId,
type FanavaranClaimProduct, type FanavaranClaimProduct,
} from "./fanavaran-claim-product"; } from "./fanavaran-claim-product";
import {
actualPremiumFromHullPolicyRecord,
customerIdFromHullPolicyRecord,
FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID,
fanavaranHullNestedClaimId,
toFanavaranHullBaseClaimPayload,
} from "./fanavaran-hull-base-claim";
import { import {
collectFanavaranExpertiseReadinessWarnings, collectFanavaranExpertiseReadinessWarnings,
hullExpertiseAssertFields, hullExpertiseAssertFields,
mergeHullVehicleIdentityFromFanavaranVehicle,
pickHullVehicleIdentity, pickHullVehicleIdentity,
toFanavaranHullExpertiseDmgSection, toFanavaranHullExpertiseDmgSection,
toFanavaranHullExpertisePayload, toFanavaranHullExpertisePayload,
vehicleCurrentValueFromHullPolicyRecord,
} from "./fanavaran-hull-expertise"; } 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 { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto"; import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
import { 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: { private async buildFanavaranDamageCasePayload(input: {
claimCase: any; claimCase: any;
blameCase?: 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) { if (persistPayload) {
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
$set: { $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 fileTypeId = profile.defaults.ClaimFileTypeId;
const product = await this.resolveFanavaranClaimProduct(claimCaseId); const product = await this.resolveFanavaranClaimProduct(claimCaseId);
@@ -6166,7 +6525,7 @@ export class ClaimRequestManagementService {
const content = { const content = {
FileName: candidate.fileName, FileName: candidate.fileName,
FileTypeId: fileTypeId, FileTypeId: fileTypeId,
...(product === "car-body" ? { ClaimId: claimCase.claimId } : {}), ...(product === "car-body" ? { ClaimId: nestedClaimId } : {}),
}; };
this.logger.log( this.logger.log(
@@ -6188,7 +6547,7 @@ export class ClaimRequestManagementService {
results.push({ results.push({
attempted: true, attempted: true,
submitted: true, submitted: true,
claimId: claimCase.claimId, claimId: nestedClaimId,
fileName: candidate.fileName, fileName: candidate.fileName,
submitUrl: url, submitUrl: url,
fanavaranResponse: response.data, fanavaranResponse: response.data,
@@ -6203,7 +6562,7 @@ export class ClaimRequestManagementService {
submitted: false, submitted: false,
warning, warning,
fileName: candidate.fileName, fileName: candidate.fileName,
claimId: claimCase.claimId, claimId: nestedClaimId,
submitUrl: url, submitUrl: url,
}); });
@@ -6752,33 +7111,19 @@ export class ClaimRequestManagementService {
String(input.claimCase.blameRequestId), String(input.claimCase.blameRequestId),
) )
: null; : null;
const hullSections = parts.map((part: any) => return this.buildCarBodyFanavaranExpertisePayload({
toFanavaranHullExpertiseDmgSection({ clientKey: input.clientKey,
partId: part?.partId, claimCase: input.claimCase,
desc: this.fanavaranPartLabel(part), blame,
wasteValue: this.fanavaranDaghiWasteValue(part?.daghi), profile,
amount: parts,
this.parseFanavaranMoney(part?.price) +
this.parseFanavaranMoney(part?.salary),
}),
);
return {
replyKey: active?.replyKey,
warnings,
payload: toFanavaranHullExpertisePayload({
claimExpertId, claimExpertId,
dmgAssessmentDate: this.convertToPersianDate(submittedAt), submittedAt,
inspectionTime: this.getTime24Hour(submittedAt), priceDropTotal,
wage: repairWage, priceDropCarPrice: priceDrop?.carPrice,
componentReplacementCost, warnings,
wasteValue, replyKey: active?.replyKey,
dropAmount: priceDropTotal || 0, });
vehicleCurrentValue:
this.parseFanavaranMoney(priceDrop?.carPrice) || null,
vehicle: pickHullVehicleIdentity(blame),
dmgSections: hullSections,
}),
};
} }
const payload: Record<string, unknown> = { 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 startedAt = Date.now();
const auditSession: FanavaranAuditSession = { const auditSession: FanavaranAuditSession = {
trackingCode: this.fanavaranAuditService.generateTrackingCode(), 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( private async postFanavaranMultipart(
url: string, url: string,
content: Record<string, unknown>, content: Record<string, unknown>,

View File

@@ -0,0 +1,79 @@
import {
actualPremiumFromHullPolicyRecord,
customerIdFromHullPolicyRecord,
FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID,
fanavaranHullNestedClaimId,
toFanavaranHullBaseClaimPayload,
} from "./fanavaran-hull-base-claim";
describe("fanavaran hull base claim", () => {
it("reads ActualPremium and CustomerId from hull policy GET", () => {
expect(
actualPremiumFromHullPolicyRecord({ TotalPremium: 21613552 }),
).toBe(21613552);
expect(customerIdFromHullPolicyRecord({ CustomerId: 1230091 })).toBe(
1230091,
);
});
it("defaults CustomerFaultPercent to 100 when CulpritTypeId is 300", () => {
const hull = toFanavaranHullBaseClaimPayload({
PolicyId: 1,
CulpritTypeId: 300,
});
expect(hull.CustomerFaultPercent).toBe(100);
});
it("uses GEN.03 Id for nested files/expertise, not ClaimNo", () => {
expect(
fanavaranHullNestedClaimId({ Id: 5023617, ClaimNo: 2268 }),
).toBe(5023617);
expect(
fanavaranHullNestedClaimId({ claimId: 5023617, claimNo: 2268 }),
).toBe(5023617);
expect(fanavaranHullNestedClaimId({ ClaimNo: 2268 })).toBeNull();
});
it("strips ثالث-only fields from a live hull create echo", () => {
const hull = toFanavaranHullBaseClaimPayload({
AccidentCauseId: 6,
AccidentCityId: 701,
AccidentDate: "1405/06/24",
AccidentLocationAddress: "استان تهران شهر تهران",
AccidentReportTypeId: 155,
AccidentTime: "10:33",
AccidentVehicleUsedId: 1,
ActualPremium: 57270462,
AnnouncementDate: "1405/06/24",
ClaimExpertId: 154,
ClaimNo: 2268,
CompensationReferenceId: 167,
CulpritLicenceNo: "9705463515",
CulpritTypeId: 337,
CustomerFaultPercent: 100,
DamagedCount: 1,
EstimateAmount: 16000000,
HasOtherCulprit: 0,
Id: 5023617,
IsFatalAccident: 0,
IsLicenseReplacement: null,
IsPlaqueChanged: 0,
PolicyId: 13764408,
PreviousPolicyEndDate: "1404/10/23",
SanhabVersion: 6,
});
expect(hull.PolicyId).toBe(13764408);
expect(hull.AccidentTypeId).toBe(FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID);
expect(hull.IsLicenseReplaced).toBeNull();
expect(hull.CostSeparationToDmgSections).toBe(0);
expect(hull.IsOwnerChanged).toBe(0);
expect(hull.CulpritTypeId).toBe(301);
expect(hull.DamagedCount).toBeUndefined();
expect(hull.HasOtherCulprit).toBeUndefined();
expect(hull.CompensationReferenceId).toBeUndefined();
expect(hull.SanhabVersion).toBeUndefined();
expect(hull.Id).toBeUndefined();
expect(hull.ClaimNo).toBeUndefined();
});
});

View File

@@ -0,0 +1,140 @@
/** GEN.03 VHUD input fields. Do not send ثالث-only keys (DamagedCount, HasOtherCulprit, plaques, …). */
export const FANAVARAN_HULL_BASE_CLAIM_KEYS = [
"ArchiveNo",
"AccidentDate",
"AccidentTime",
"AnnouncementDate",
"AccidentLocationAddress",
"ClaimExpertId",
"EntryDate",
"CulpritLicenceNo",
"CulpritLicenceIssuDate",
"CulpritLicenceForeignCityName",
"PoliceReportSeri",
"PoliceReportSerial",
"PoliceReportDesc",
"CustomerFaultPercent",
"CulpritLevelTwoLicenceIssuDate",
"ActualPremium",
"EstimateAmount",
"TrackingCode",
"CourtArchiveNo",
"PlaqueReplacementDate",
"StatusChangeDate",
"ClaimCompletionDate",
"DmgAssessorFirstCreationTime",
"PolicyId",
"IsSurplusArticleEighthLaw",
"AccidentCityId",
"AccidentCauseId",
"AccidentTypeId",
"GlassBreakReasonId",
"CulpritTypeId",
"AuthorityCulpritId",
"AccidentCulpritId",
"CulpritLicenceTypeId",
"IsLicenseReplaced",
"CulpritLicenceCountryId",
"CulpritLicenceCityId",
"AccidentReportTypeId",
"PoliceOfficerId",
"IsAccidentOutOfBorder",
"AccidentVehicleUsedId",
"IsOwnerChanged",
"CostSeparationToDmgSections",
] as const;
/** GEN.03 default: تصادف(حادثه). Lookup: GET /lookups/vehicle-hull-accident-types */
export const FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID = 2;
/** GEN.03 ans (0=خیر). Used for CostSeparationToDmgSections, IsOwnerChanged, IsLicenseReplaced. */
export const FANAVARAN_HULL_ANS_NO = 0;
/** GEN.03 hull culprit type when third-party is at fault. Lookup: vehicle-hull-accident-culprit-type */
export const FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID = 301;
/** GEN.03 ActualPremium from GET car/vehicle-hull-policies/{PolicyId} → TotalPremium. */
export function actualPremiumFromHullPolicyRecord(
policy: Record<string, unknown> | null | undefined,
): number | null {
if (!policy) return null;
const raw = policy.TotalPremium ?? policy.totalPremium;
const n = typeof raw === "number" ? raw : Number(raw);
return Number.isFinite(n) && n >= 0 ? n : null;
}
/** Fallback AccidentCulpritId when parties inquiry is unavailable. */
export function customerIdFromHullPolicyRecord(
policy: Record<string, unknown> | null | undefined,
): number | null {
if (!policy) return null;
const raw = policy.CustomerId ?? policy.customerId;
const n = typeof raw === "number" ? raw : Number(raw);
return Number.isFinite(n) && n > 0 ? n : null;
}
export function toFanavaranHullBaseClaimPayload(
built: Record<string, unknown>,
): Record<string, unknown> {
const next: Record<string, unknown> = {};
for (const key of FANAVARAN_HULL_BASE_CLAIM_KEYS) {
if (key === "IsLicenseReplaced") {
next[key] =
built.IsLicenseReplaced ?? built.IsLicenseReplacement ?? null;
continue;
}
if (key === "AccidentTypeId") {
next[key] =
built.AccidentTypeId ?? FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID;
continue;
}
if (key === "CostSeparationToDmgSections") {
next[key] =
built.CostSeparationToDmgSections ?? FANAVARAN_HULL_ANS_NO;
continue;
}
if (key === "IsOwnerChanged") {
next[key] = built.IsOwnerChanged ?? FANAVARAN_HULL_ANS_NO;
continue;
}
if (key === "CulpritTypeId") {
const raw = built.CulpritTypeId;
// ثالث profile default 337 is not in vehicle-hull-accident-culprit-type.
if (raw == null || raw === 337) {
next[key] = FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID;
} else {
next[key] = raw;
}
continue;
}
if (key === "CustomerFaultPercent") {
const culpritType =
built.CulpritTypeId ?? FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID;
if (built.CustomerFaultPercent != null) {
next[key] = built.CustomerFaultPercent;
} else if (culpritType === 300) {
next[key] = 100;
} else {
next[key] = null;
}
continue;
}
next[key] = Object.prototype.hasOwnProperty.call(built, key)
? built[key]
: null;
}
return next;
}
/** Nested hull files/expertise use GEN.03 `Id` (کد رایانه), never `ClaimNo` (شماره پرونده). */
export function fanavaranHullNestedClaimId(input: {
Id?: unknown;
ClaimNo?: unknown;
claimId?: unknown;
claimNo?: unknown;
}): number | null {
const id = input.claimId ?? input.Id;
if (typeof id === "number" && Number.isFinite(id) && id > 0) return id;
if (typeof id === "string" && /^\d+$/.test(id.trim())) return Number(id);
return null;
}

View File

@@ -0,0 +1,76 @@
import { parseFanavaranId } from "src/lookups/fanavaran-last-car-policy";
export const FANAVARAN_HULL_DMG_KIND_CAPTION = "تخریب";
export const FANAVARAN_HULL_DMG_COST_KIND_CAPTION = "مجموع لوازم";
/** Fallback when lookup fetch fails (Parsian `vehicle-hull-dmg-kind`). */
export const FANAVARAN_DEFAULT_HULL_DMG_KIND_ID = 5485;
/** Fallback when lookup fetch fails (`vehicle-hull-dmg-cost-kinds` Id). */
export const FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID = 1;
export type FanavaranHullDmgAccessoryRow = {
Id?: unknown;
AccessoryKindId?: unknown;
AccessoryDesc?: unknown;
};
export function asLookupRows(value: unknown): Record<string, unknown>[] {
if (!Array.isArray(value)) return [];
return value.filter(
(row): row is Record<string, unknown> =>
!!row && typeof row === "object" && !Array.isArray(row),
);
}
export function asHullDmgAccessoryRows(value: unknown): FanavaranHullDmgAccessoryRow[] {
return asLookupRows(value) as FanavaranHullDmgAccessoryRow[];
}
export function findLookupIdByCaption(
rows: unknown,
caption: string,
): number | null {
const target = caption.trim();
for (const row of asLookupRows(rows)) {
if (String(row.Caption ?? "").trim() !== target) continue;
const id = parseFanavaranId(row.Id);
if (id != null) return id;
}
return null;
}
function isFactoryDefaultAccessoryRow(row: FanavaranHullDmgAccessoryRow): boolean {
const desc = String(row.AccessoryDesc ?? "").trim();
return (
desc.includes("کليه قطعات فابريک") ||
desc.includes("کلیه قطعات فابریک") ||
desc.includes("کليه قطعات") ||
desc.includes("فابريک")
);
}
/**
* GEN.06 VehicleHullAccessoryId from policy dmg-accessories.
* 1) Match AccessoryKindId to car-components part id.
* 2) Else factory bundle row (app default outer parts map here on Parsian).
*/
export function resolveVehicleHullAccessoryId(
accessories: FanavaranHullDmgAccessoryRow[],
accessoryKindId: number | null,
): number | null {
if (accessories.length === 0) return null;
if (accessoryKindId != null) {
const exact = accessories.find(
(row) => parseFanavaranId(row.AccessoryKindId) === accessoryKindId,
);
const exactId = parseFanavaranId(exact?.Id);
if (exactId != null) return exactId;
}
const factory = accessories.find(isFactoryDefaultAccessoryRow);
const factoryId = parseFanavaranId(factory?.Id);
if (factoryId != null) return factoryId;
return parseFanavaranId(accessories[0]?.Id);
}

View File

@@ -5,9 +5,16 @@ import {
pickHullVehicleIdentity, pickHullVehicleIdentity,
toFanavaranHullExpertiseDmgSection, toFanavaranHullExpertiseDmgSection,
toFanavaranHullExpertisePayload, toFanavaranHullExpertisePayload,
vehicleCurrentValueFromHullPolicyRecord,
} from "./fanavaran-hull-expertise"; } from "./fanavaran-hull-expertise";
describe("fanavaran hull expertise", () => { describe("fanavaran hull expertise", () => {
it("reads VehicleCurrentValue from hull policy VehicleValue", () => {
expect(
vehicleCurrentValueFromHullPolicyRecord({ VehicleValue: 11000000000 }),
).toBe(11000000000);
});
it("maps car-body inquiry identity onto GEN.06 vehicle fields", () => { it("maps car-body inquiry identity onto GEN.06 vehicle fields", () => {
const vehicle = pickHullVehicleIdentity({ const vehicle = pickHullVehicleIdentity({
parties: [ parties: [
@@ -71,6 +78,9 @@ describe("fanavaran hull expertise", () => {
desc: "bumper", desc: "bumper",
wasteValue: 0, wasteValue: 0,
amount: 3000, amount: 3000,
dmgKindId: 5485,
dmgCostKindId: 1,
vehicleHullAccessoryId: 3043330,
}), }),
], ],
}); });
@@ -89,10 +99,10 @@ describe("fanavaran hull expertise", () => {
Desc: "bumper", Desc: "bumper",
WasteValue: 0, WasteValue: 0,
AccessoryKindId: 9, AccessoryKindId: 9,
DmgKindId: null, DmgKindId: 5485,
VehicleHullAccessoryId: null, VehicleHullAccessoryId: 3043330,
DmgSectionCosts: [ DmgSectionCosts: [
{ Caption: "bumper", Amount: 3000, DmgCostKindId: null }, { Caption: "bumper", Amount: 3000, DmgCostKindId: 1 },
], ],
}, },
]); ]);
@@ -122,12 +132,18 @@ describe("fanavaran hull expertise", () => {
desc: "سپر جلو", desc: "سپر جلو",
wasteValue: 0, wasteValue: 0,
amount: 11000000, amount: 11000000,
dmgKindId: 5485,
dmgCostKindId: 1,
vehicleHullAccessoryId: 3043330,
}), }),
toFanavaranHullExpertiseDmgSection({ toFanavaranHullExpertiseDmgSection({
partId: 11, partId: 11,
desc: "آينه سمت راننده", desc: "آينه سمت راننده",
wasteValue: 0, wasteValue: 0,
amount: 5000000, amount: 5000000,
dmgKindId: 5485,
dmgCostKindId: 1,
vehicleHullAccessoryId: 3043330,
}), }),
], ],
}); });

View File

@@ -13,6 +13,29 @@ export type FanavaranHullVehicleIdentity = {
builtYear: number | null; builtYear: number | null;
}; };
export function mergeHullVehicleIdentityFromFanavaranVehicle(
base: FanavaranHullVehicleIdentity,
vehicle: Record<string, unknown> | null | undefined,
): FanavaranHullVehicleIdentity {
if (!vehicle) return base;
const readText = (key: string, fallback: string | null) => {
const raw = vehicle[key];
if (raw == null || String(raw).trim() === "") return fallback;
return String(raw).trim();
};
return {
motorNo: readText("MotorNo", base.motorNo),
chassisNo: readText("ChassisNo", base.chassisNo),
vin: readText("VIN", base.vin),
plaqueNo: readText("PlaqueNo", base.plaqueNo),
plaqueSerial: readText("PlaqueSerial", base.plaqueSerial),
builtYear:
parseFanavaranId(vehicle.BuiltYear) ??
parseFanavaranId(vehicle.builtYear) ??
base.builtYear,
};
}
export function pickHullVehicleIdentity( export function pickHullVehicleIdentity(
blame?: { parties?: unknown[] } | null, blame?: { parties?: unknown[] } | null,
): FanavaranHullVehicleIdentity { ): FanavaranHullVehicleIdentity {
@@ -48,6 +71,24 @@ function text(value: unknown): string | null {
return next ? next : null; return next ? next : null;
} }
export {
FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID,
FANAVARAN_DEFAULT_HULL_DMG_KIND_ID,
FANAVARAN_HULL_DMG_COST_KIND_CAPTION,
FANAVARAN_HULL_DMG_KIND_CAPTION,
} from "./fanavaran-hull-expertise-lookups";
/** GEN.06 VehicleCurrentValue from GET car/vehicle-hull-policies/{PolicyId} → VehicleValue. */
export function vehicleCurrentValueFromHullPolicyRecord(
policy: Record<string, unknown> | null | undefined,
): number | null {
if (!policy) return null;
const raw =
policy.VehicleValue ?? policy.vehicleValue ?? policy.VehicleCurrentValue;
const n = typeof raw === "number" ? raw : Number(raw);
return Number.isFinite(n) && n > 0 ? n : null;
}
export function toFanavaranHullExpertisePayload(input: { export function toFanavaranHullExpertisePayload(input: {
claimExpertId: number; claimExpertId: number;
dmgAssessmentDate: string; dmgAssessmentDate: string;
@@ -58,6 +99,8 @@ export function toFanavaranHullExpertisePayload(input: {
dropAmount: number; dropAmount: number;
vehicleCurrentValue: number | null; vehicleCurrentValue: number | null;
vehicle: FanavaranHullVehicleIdentity; vehicle: FanavaranHullVehicleIdentity;
colorId?: number | null;
repairDuration?: number | null;
dmgSections: Record<string, unknown>[]; dmgSections: Record<string, unknown>[];
}): Record<string, unknown> { }): Record<string, unknown> {
return { return {
@@ -74,7 +117,7 @@ export function toFanavaranHullExpertisePayload(input: {
ComponentReplacementCost: input.componentReplacementCost, ComponentReplacementCost: input.componentReplacementCost,
WasteValue: input.wasteValue, WasteValue: input.wasteValue,
CarryAndRescueCost: null, CarryAndRescueCost: null,
RepairDuration: null, RepairDuration: input.repairDuration ?? null,
VehicleCurrentValue: input.vehicleCurrentValue, VehicleCurrentValue: input.vehicleCurrentValue,
WreckHighestValue: null, WreckHighestValue: null,
InspectionDeduction: null, InspectionDeduction: null,
@@ -82,7 +125,7 @@ export function toFanavaranHullExpertisePayload(input: {
WentDistanceByExpert: null, WentDistanceByExpert: null,
IsDestruction: null, IsDestruction: null,
DropAmount: input.dropAmount, DropAmount: input.dropAmount,
ColorId: null, ColorId: input.colorId ?? null,
PlaqueDesignId: null, PlaqueDesignId: null,
PlaqueCityId: null, PlaqueCityId: null,
AccidentPercent: null, AccidentPercent: null,
@@ -96,19 +139,22 @@ export function toFanavaranHullExpertiseDmgSection(input: {
desc: string; desc: string;
wasteValue: number; wasteValue: number;
amount: number; amount: number;
dmgKindId: number;
dmgCostKindId: number;
vehicleHullAccessoryId: number | null;
}): Record<string, unknown> { }): Record<string, unknown> {
return { return {
Count: 1, Count: 1,
Desc: input.desc, Desc: input.desc,
WasteValue: input.wasteValue, WasteValue: input.wasteValue,
AccessoryKindId: parseFanavaranId(input.partId), AccessoryKindId: parseFanavaranId(input.partId),
DmgKindId: null, DmgKindId: input.dmgKindId,
VehicleHullAccessoryId: null, VehicleHullAccessoryId: input.vehicleHullAccessoryId,
DmgSectionCosts: [ DmgSectionCosts: [
{ {
Caption: input.desc, Caption: input.desc,
Amount: input.amount, Amount: input.amount,
DmgCostKindId: null, DmgCostKindId: input.dmgCostKindId,
}, },
], ],
}; };

View File

@@ -57,6 +57,8 @@ export interface FanavaranPayloadDefaults {
* (Parsian: 29; Tejaratno proven: 2709). * (Parsian: 29; Tejaratno proven: 2709).
*/ */
ExpertiseClaimExpertId: number; ExpertiseClaimExpertId: number;
/** GEN.06 hull RepairDuration when configured per tenant (optional). */
HullExpertiseRepairDuration?: number | null;
CompensationReferenceId: number; CompensationReferenceId: number;
CulpritLicenceTypeId: number; CulpritLicenceTypeId: number;
CulpritTypeId: number; CulpritTypeId: number;

View File

@@ -16,6 +16,26 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/accident-causes`, url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/accident-causes`,
cacheFile: "accident-causes.json", cacheFile: "accident-causes.json",
}, },
{
name: "vehicle-hull-accident-types",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/vehicle-hull-accident-types`,
cacheFile: "vehicle-hull-accident-types.json",
},
{
name: "vehicle-hull-accident-culprit-type",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-accident-culprit-type`,
cacheFile: "vehicle-hull-accident-culprit-type.json",
},
{
name: "vehicle-hull-dmg-kind",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-dmg-kind`,
cacheFile: "vehicle-hull-dmg-kind.json",
},
{
name: "vehicle-hull-dmg-cost-kinds",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/vehicle-hull-dmg-cost-kinds`,
cacheFile: "vehicle-hull-dmg-cost-kinds.json",
},
{ {
name: "accident-report-type", name: "accident-report-type",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-report-type`, url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-report-type`,

View File

@@ -277,6 +277,16 @@ export class FanavaranLookupService {
return this.fetchFromFanavaran(clientKey, url, options); return this.fetchFromFanavaran(clientKey, url, options);
} }
/** GEN.06 VehicleHullAccessoryId — requires hull PolicyId (کد رایانه بیمه‌نامه). */
async vehicleHullDmgAccessoriesByPolicyId(
clientKey: FanavaranClientKey,
policyId: number,
options?: { contractIdOverride?: string },
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/vehicle-hull-policies/${policyId}/dmg-accessories`;
return this.fetchFromFanavaran(clientKey, url, options);
}
async vehicleById( async vehicleById(
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
vehicleId: number, vehicleId: number,

View File

@@ -36,6 +36,85 @@ export class LookupsController {
return await this.lookupsService.getAccidentCauses(); return await this.lookupsService.getAccidentCauses();
} }
@Get("vehicle-hull-accident-types")
@ApiOperation({
summary: "Fanavaran GEN.03 hull accident type lookup",
description:
"Returns values for base-claim field AccidentTypeId from car/base-info/vehicle-hull-accident-types (not the Tejarat static accident-type list).",
})
@ApiOkResponse({
description: "Returns Fanavaran vehicle hull accident types",
schema: { type: "array", items: { type: "object" } },
})
async getVehicleHullAccidentTypes() {
return await this.lookupsService.getVehicleHullAccidentTypes();
}
@Get("vehicle-hull-accident-culprit-type")
@ApiOperation({
summary: "Fanavaran GEN.03 hull culprit type lookup",
description:
"Returns values for base-claim field CulpritTypeId from car/code-list/vehicle-hull-accident-culprit-type (not accident-culprit-type used for ثالث).",
})
@ApiOkResponse({
description: "Returns Fanavaran vehicle hull accident culprit types",
schema: { type: "array", items: { type: "object" } },
})
async getVehicleHullAccidentCulpritType() {
return await this.lookupsService.getVehicleHullAccidentCulpritType();
}
@Get("vehicle-hull-dmg-kind")
@ApiOperation({
summary: "Fanavaran GEN.06 hull damage kind lookup",
description:
"Returns values for DmgSections[].DmgKindId from car/code-list/vehicle-hull-dmg-kind.",
})
@ApiOkResponse({
description: "Returns Fanavaran vehicle hull damage kinds",
schema: { type: "array", items: { type: "object" } },
})
async getVehicleHullDmgKind() {
return await this.lookupsService.getVehicleHullDmgKind();
}
@Get("vehicle-hull-dmg-cost-kinds")
@ApiOperation({
summary: "Fanavaran GEN.06 hull damage cost kind lookup",
description:
"Returns values for DmgSections[].DmgSectionCosts[].DmgCostKindId from car/base-info/vehicle-hull-dmg-cost-kinds.",
})
@ApiOkResponse({
description: "Returns Fanavaran vehicle hull damage cost kinds",
schema: { type: "array", items: { type: "object" } },
})
async getVehicleHullDmgCostKinds() {
return await this.lookupsService.getVehicleHullDmgCostKinds();
}
@Get("vehicle-hull-dmg-accessories/:policyId")
@ApiOperation({
summary: "Fanavaran GEN.06 hull policy damage accessories",
description:
"Returns rows for DmgSections[].VehicleHullAccessoryId from car/vehicle-hull-policies/{policyId}/dmg-accessories. PolicyId is required (Fanavaran: کد رایانه ریسورس).",
})
@ApiParam({
name: "policyId",
description: "Fanavaran hull policy Id (same as GEN.03 PolicyId)",
example: 13764610,
})
@ApiOkResponse({
description: "Returns Fanavaran vehicle hull damage accessories for the policy",
schema: { type: "array", items: { type: "object" } },
})
async getVehicleHullDmgAccessoriesByPolicyId(
@Param("policyId", ParseIntPipe) policyId: number,
) {
return await this.lookupsService.getVehicleHullDmgAccessoriesByPolicyId(
policyId,
);
}
@Get("accident-report-type") @Get("accident-report-type")
@ApiOkResponse({ @ApiOkResponse({
description: "Returns accident report type lookup data", description: "Returns accident report type lookup data",

View File

@@ -106,6 +106,36 @@ export class LookupsService {
return await this.getClientRemoteLookup("accident-causes"); return await this.getClientRemoteLookup("accident-causes");
} }
async getVehicleHullAccidentTypes(): Promise<any> {
return await this.getClientRemoteLookup("vehicle-hull-accident-types");
}
async getVehicleHullAccidentCulpritType(): Promise<any> {
return await this.getClientRemoteLookup("vehicle-hull-accident-culprit-type");
}
async getVehicleHullDmgKind(): Promise<any> {
return await this.getClientRemoteLookup("vehicle-hull-dmg-kind");
}
async getVehicleHullDmgCostKinds(): Promise<any> {
return await this.getClientRemoteLookup("vehicle-hull-dmg-cost-kinds");
}
async getVehicleHullDmgAccessoriesByPolicyId(policyId: number): Promise<unknown> {
const clientKey = this.activeClientKey();
return this.fanavaranLookupService.vehicleHullDmgAccessoriesByPolicyId(
clientKey,
policyId,
{
contractIdOverride: resolveFanavaranProductContractId(
clientKey,
"car-body",
),
},
);
}
async getAccidentReportType(): Promise<any> { async getAccidentReportType(): Promise<any> {
return await this.getClientRemoteLookup("accident-report-type"); return await this.getClientRemoteLookup("accident-report-type");
} }