forked from Yara724/api
Compare commits
3 Commits
a174332d81
...
a5f534817b
| Author | SHA1 | Date | |
|---|---|---|---|
| a5f534817b | |||
| ebfb4385de | |||
| acb5d2a682 |
@@ -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 {
|
||||
@@ -123,6 +141,13 @@ import { PublicIdService } from "src/utils/public-id/public-id.service";
|
||||
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
|
||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
|
||||
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
||||
import {
|
||||
assertFileMakerCanCreateBlameType,
|
||||
assertFileReviewerCanReviewBlameType,
|
||||
resolveFileMakerStageOneExpertId,
|
||||
resolveFileReviewerExpertiseExpertId,
|
||||
} from "src/fanavaran/fanavaran-file-role-expert-ids";
|
||||
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
|
||||
import { FanavaranLocationService } from "src/fanavaran/fanavaran-location.service";
|
||||
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
|
||||
@@ -456,6 +481,7 @@ export class ClaimRequestManagementService {
|
||||
private readonly fanavaranLookupService: FanavaranLookupService,
|
||||
private readonly smsOrchestrationService: SmsOrchestrationService,
|
||||
private readonly fileMakerDbService: FileMakerDbService,
|
||||
private readonly fileReviewerDbService: FileReviewerDbService,
|
||||
private readonly fieldExpertDbService: FieldExpertDbService,
|
||||
private readonly plateNormalizer: PlateNormalizerService,
|
||||
private readonly fanavaranLocationService: FanavaranLocationService,
|
||||
@@ -4219,6 +4245,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;
|
||||
@@ -4860,6 +5187,55 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V4/V5 FileMaker flows: GEN.03 ClaimExpertId from file-maker; GEN.06 from file-reviewer.
|
||||
* Returns null for non–file-maker blames (tenant defaults apply).
|
||||
*/
|
||||
private async resolveFanavaranFileRoleExpertIdsForBlame(blame: {
|
||||
isMadeByFileMaker?: boolean;
|
||||
type?: BlameRequestType;
|
||||
initiatedByFieldExpertId?: unknown;
|
||||
assignedFileReviewerId?: unknown;
|
||||
}): Promise<{
|
||||
baseClaimExpertId: number | null;
|
||||
expertiseClaimExpertId: number | null;
|
||||
} | null> {
|
||||
if (!blame?.isMadeByFileMaker || blame.type == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let baseClaimExpertId: number | null = null;
|
||||
let expertiseClaimExpertId: number | null = null;
|
||||
|
||||
if (blame.initiatedByFieldExpertId) {
|
||||
const fileMaker = await this.fileMakerDbService.findById(
|
||||
String(blame.initiatedByFieldExpertId),
|
||||
);
|
||||
if (fileMaker) {
|
||||
assertFileMakerCanCreateBlameType(fileMaker, blame.type);
|
||||
baseClaimExpertId = resolveFileMakerStageOneExpertId(
|
||||
fileMaker,
|
||||
blame.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (blame.assignedFileReviewerId) {
|
||||
const fileReviewer = await this.fileReviewerDbService.findById(
|
||||
String(blame.assignedFileReviewerId),
|
||||
);
|
||||
if (fileReviewer) {
|
||||
assertFileReviewerCanReviewBlameType(fileReviewer, blame.type);
|
||||
expertiseClaimExpertId = resolveFileReviewerExpertiseExpertId(
|
||||
fileReviewer,
|
||||
blame.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { baseClaimExpertId, expertiseClaimExpertId };
|
||||
}
|
||||
|
||||
private async getPolicyIdFromNationalCode(
|
||||
nationalCodeOfInsurer: string,
|
||||
config: {
|
||||
@@ -5743,6 +6119,32 @@ 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);
|
||||
}
|
||||
|
||||
const fileRoleExpertIds =
|
||||
await this.resolveFanavaranFileRoleExpertIdsForBlame(blameCase);
|
||||
if (fileRoleExpertIds) {
|
||||
if (fileRoleExpertIds.baseClaimExpertId == null) {
|
||||
throw new BadRequestException({
|
||||
code: "FILE_MAKER_FANAVARAN_EXPERT_CODE_MISSING",
|
||||
message:
|
||||
"This FileMaker file cannot be submitted to Fanavaran: stage-1 claim expert id is missing for this product line.",
|
||||
});
|
||||
}
|
||||
payload.ClaimExpertId = fileRoleExpertIds.baseClaimExpertId;
|
||||
}
|
||||
|
||||
if (persistPayload) {
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
|
||||
$set: {
|
||||
@@ -6153,7 +6555,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 +6595,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 +6617,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 +6632,7 @@ export class ClaimRequestManagementService {
|
||||
submitted: false,
|
||||
warning,
|
||||
fileName: candidate.fileName,
|
||||
claimId: claimCase.claimId,
|
||||
claimId: nestedClaimId,
|
||||
submitUrl: url,
|
||||
});
|
||||
|
||||
@@ -6689,10 +7118,35 @@ export class ClaimRequestManagementService {
|
||||
const profile = getFanavaranClientProfile(input.clientKey);
|
||||
const active = getActiveV2ExpertReply(input.claimCase as any);
|
||||
|
||||
// GEN.08 ClaimExpertId = tenant ExpertiseClaimExpertId (assessor role).
|
||||
// Tejaratno: 2709. Parsian: 29. Must NOT reuse GEN.03 ClaimExpertId
|
||||
// (مسئول پرونده مالی — Tejaratno 4543092 / Parsian 154).
|
||||
const claimExpertId = profile.defaults.ExpertiseClaimExpertId;
|
||||
const blameForExpertIds = input.claimCase?.blameRequestId
|
||||
? await this.blameRequestDbService.findById(
|
||||
String(input.claimCase.blameRequestId),
|
||||
)
|
||||
: null;
|
||||
|
||||
// GEN.06 ClaimExpertId = tenant ExpertiseClaimExpertId (assessor role), unless
|
||||
// V4/V5 FileMaker flow → file-reviewer expertise code for this product line.
|
||||
let claimExpertId = profile.defaults.ExpertiseClaimExpertId;
|
||||
if (blameForExpertIds) {
|
||||
const roleExpertIds =
|
||||
await this.resolveFanavaranFileRoleExpertIdsForBlame(
|
||||
blameForExpertIds as {
|
||||
isMadeByFileMaker?: boolean;
|
||||
type?: BlameRequestType;
|
||||
initiatedByFieldExpertId?: unknown;
|
||||
assignedFileReviewerId?: unknown;
|
||||
},
|
||||
);
|
||||
if (roleExpertIds?.expertiseClaimExpertId != null) {
|
||||
claimExpertId = roleExpertIds.expertiseClaimExpertId;
|
||||
} else if (roleExpertIds) {
|
||||
throw new BadRequestException({
|
||||
code: "FILE_REVIEWER_FANAVARAN_EXPERT_CODE_MISSING",
|
||||
message:
|
||||
"Fanavaran expertise cannot be submitted: assign a FileReviewer with an expertise claim expert id for this product line.",
|
||||
});
|
||||
}
|
||||
}
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!active?.parts?.length) {
|
||||
@@ -6747,38 +7201,20 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
|
||||
if (product === "car-body") {
|
||||
const blame = input.claimCase?.blameRequestId
|
||||
? await this.blameRequestDbService.findById(
|
||||
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,
|
||||
const blame = blameForExpertIds;
|
||||
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 +7376,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 +7779,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>,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
140
src/claim-request-management/fanavaran-hull-base-claim.ts
Normal file
140
src/claim-request-management/fanavaran-hull-base-claim.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -5,9 +5,16 @@ import {
|
||||
pickHullVehicleIdentity,
|
||||
toFanavaranHullExpertiseDmgSection,
|
||||
toFanavaranHullExpertisePayload,
|
||||
vehicleCurrentValueFromHullPolicyRecord,
|
||||
} from "./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", () => {
|
||||
const vehicle = pickHullVehicleIdentity({
|
||||
parties: [
|
||||
@@ -71,6 +78,9 @@ describe("fanavaran hull expertise", () => {
|
||||
desc: "bumper",
|
||||
wasteValue: 0,
|
||||
amount: 3000,
|
||||
dmgKindId: 5485,
|
||||
dmgCostKindId: 1,
|
||||
vehicleHullAccessoryId: 3043330,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -89,10 +99,10 @@ describe("fanavaran hull expertise", () => {
|
||||
Desc: "bumper",
|
||||
WasteValue: 0,
|
||||
AccessoryKindId: 9,
|
||||
DmgKindId: null,
|
||||
VehicleHullAccessoryId: null,
|
||||
DmgKindId: 5485,
|
||||
VehicleHullAccessoryId: 3043330,
|
||||
DmgSectionCosts: [
|
||||
{ Caption: "bumper", Amount: 3000, DmgCostKindId: null },
|
||||
{ Caption: "bumper", Amount: 3000, DmgCostKindId: 1 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -122,12 +132,18 @@ describe("fanavaran hull expertise", () => {
|
||||
desc: "سپر جلو",
|
||||
wasteValue: 0,
|
||||
amount: 11000000,
|
||||
dmgKindId: 5485,
|
||||
dmgCostKindId: 1,
|
||||
vehicleHullAccessoryId: 3043330,
|
||||
}),
|
||||
toFanavaranHullExpertiseDmgSection({
|
||||
partId: 11,
|
||||
desc: "آينه سمت راننده",
|
||||
wasteValue: 0,
|
||||
amount: 5000000,
|
||||
dmgKindId: 5485,
|
||||
dmgCostKindId: 1,
|
||||
vehicleHullAccessoryId: 3043330,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -13,6 +13,29 @@ export type FanavaranHullVehicleIdentity = {
|
||||
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(
|
||||
blame?: { parties?: unknown[] } | null,
|
||||
): FanavaranHullVehicleIdentity {
|
||||
@@ -48,6 +71,24 @@ function text(value: unknown): string | 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: {
|
||||
claimExpertId: number;
|
||||
dmgAssessmentDate: string;
|
||||
@@ -58,6 +99,8 @@ export function toFanavaranHullExpertisePayload(input: {
|
||||
dropAmount: number;
|
||||
vehicleCurrentValue: number | null;
|
||||
vehicle: FanavaranHullVehicleIdentity;
|
||||
colorId?: number | null;
|
||||
repairDuration?: number | null;
|
||||
dmgSections: Record<string, unknown>[];
|
||||
}): Record<string, unknown> {
|
||||
return {
|
||||
@@ -74,7 +117,7 @@ export function toFanavaranHullExpertisePayload(input: {
|
||||
ComponentReplacementCost: input.componentReplacementCost,
|
||||
WasteValue: input.wasteValue,
|
||||
CarryAndRescueCost: null,
|
||||
RepairDuration: null,
|
||||
RepairDuration: input.repairDuration ?? null,
|
||||
VehicleCurrentValue: input.vehicleCurrentValue,
|
||||
WreckHighestValue: null,
|
||||
InspectionDeduction: null,
|
||||
@@ -82,7 +125,7 @@ export function toFanavaranHullExpertisePayload(input: {
|
||||
WentDistanceByExpert: null,
|
||||
IsDestruction: null,
|
||||
DropAmount: input.dropAmount,
|
||||
ColorId: null,
|
||||
ColorId: input.colorId ?? null,
|
||||
PlaqueDesignId: null,
|
||||
PlaqueCityId: null,
|
||||
AccidentPercent: null,
|
||||
@@ -96,19 +139,22 @@ export function toFanavaranHullExpertiseDmgSection(input: {
|
||||
desc: string;
|
||||
wasteValue: number;
|
||||
amount: number;
|
||||
dmgKindId: number;
|
||||
dmgCostKindId: number;
|
||||
vehicleHullAccessoryId: number | null;
|
||||
}): Record<string, unknown> {
|
||||
return {
|
||||
Count: 1,
|
||||
Desc: input.desc,
|
||||
WasteValue: input.wasteValue,
|
||||
AccessoryKindId: parseFanavaranId(input.partId),
|
||||
DmgKindId: null,
|
||||
VehicleHullAccessoryId: null,
|
||||
DmgKindId: input.dmgKindId,
|
||||
VehicleHullAccessoryId: input.vehicleHullAccessoryId,
|
||||
DmgSectionCosts: [
|
||||
{
|
||||
Caption: input.desc,
|
||||
Amount: input.amount,
|
||||
DmgCostKindId: null,
|
||||
DmgCostKindId: input.dmgCostKindId,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -57,6 +57,8 @@ export interface FanavaranPayloadDefaults {
|
||||
* (Parsian: 29; Tejaratno proven: 2709).
|
||||
*/
|
||||
ExpertiseClaimExpertId: number;
|
||||
/** GEN.06 hull RepairDuration when configured per tenant (optional). */
|
||||
HullExpertiseRepairDuration?: number | null;
|
||||
CompensationReferenceId: number;
|
||||
CulpritLicenceTypeId: number;
|
||||
CulpritTypeId: number;
|
||||
|
||||
57
src/fanavaran/fanavaran-file-role-expert-ids.spec.ts
Normal file
57
src/fanavaran/fanavaran-file-role-expert-ids.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import {
|
||||
assertFileMakerCanCreateBlameType,
|
||||
assertFileReviewerCanReviewBlameType,
|
||||
resolveFileMakerStageOneExpertId,
|
||||
resolveFileReviewerExpertiseExpertId,
|
||||
} from "./fanavaran-file-role-expert-ids";
|
||||
|
||||
describe("fanavaran-file-role-expert-ids", () => {
|
||||
it("resolves stage-1 and expertise ids by blame type", () => {
|
||||
expect(
|
||||
resolveFileMakerStageOneExpertId(
|
||||
{
|
||||
ThirdPartyClaimExpertId: "154",
|
||||
CarBodyClaimExpertId: "29",
|
||||
},
|
||||
BlameRequestType.THIRD_PARTY,
|
||||
),
|
||||
).toBe(154);
|
||||
expect(
|
||||
resolveFileMakerStageOneExpertId(
|
||||
{
|
||||
ThirdPartyClaimExpertId: "154",
|
||||
CarBodyClaimExpertId: "29",
|
||||
},
|
||||
BlameRequestType.CAR_BODY,
|
||||
),
|
||||
).toBe(29);
|
||||
expect(
|
||||
resolveFileReviewerExpertiseExpertId(
|
||||
{
|
||||
ThirdPartyExpertiseClaim: "2709",
|
||||
CarBodyExpertiseClaim: "15",
|
||||
},
|
||||
BlameRequestType.CAR_BODY,
|
||||
),
|
||||
).toBe(15);
|
||||
});
|
||||
|
||||
it("blocks file maker create when the product code is missing", () => {
|
||||
expect(() =>
|
||||
assertFileMakerCanCreateBlameType(
|
||||
{ ThirdPartyClaimExpertId: "154" },
|
||||
BlameRequestType.CAR_BODY,
|
||||
),
|
||||
).toThrow(/stage-1 claim expert id/);
|
||||
});
|
||||
|
||||
it("blocks file reviewer when the product code is missing", () => {
|
||||
expect(() =>
|
||||
assertFileReviewerCanReviewBlameType(
|
||||
{ CarBodyExpertiseClaim: "15" },
|
||||
BlameRequestType.THIRD_PARTY,
|
||||
),
|
||||
).toThrow(/expertise claim expert id/);
|
||||
});
|
||||
});
|
||||
90
src/fanavaran/fanavaran-file-role-expert-ids.ts
Normal file
90
src/fanavaran/fanavaran-file-role-expert-ids.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { parseFanavaranId } from "src/lookups/fanavaran-last-car-policy";
|
||||
|
||||
export type FileMakerFanavaranExpertCodes = {
|
||||
ThirdPartyClaimExpertId?: string;
|
||||
CarBodyClaimExpertId?: string;
|
||||
};
|
||||
|
||||
export type FileReviewerFanavaranExpertCodes = {
|
||||
ThirdPartyExpertiseClaim?: string;
|
||||
CarBodyExpertiseClaim?: string;
|
||||
};
|
||||
|
||||
export function parseFanavaranExpertId(raw?: string | null): number | null {
|
||||
if (raw == null) return null;
|
||||
const trimmed = String(raw).trim();
|
||||
if (!trimmed) return null;
|
||||
return parseFanavaranId(trimmed) ?? parseFanavaranId(Number(trimmed));
|
||||
}
|
||||
|
||||
export function fileMakerStageOneExpertCode(
|
||||
fileMaker: FileMakerFanavaranExpertCodes,
|
||||
blameType: BlameRequestType,
|
||||
): string | undefined {
|
||||
return blameType === BlameRequestType.CAR_BODY
|
||||
? fileMaker.CarBodyClaimExpertId
|
||||
: fileMaker.ThirdPartyClaimExpertId;
|
||||
}
|
||||
|
||||
export function fileReviewerExpertiseExpertCode(
|
||||
fileReviewer: FileReviewerFanavaranExpertCodes,
|
||||
blameType: BlameRequestType,
|
||||
): string | undefined {
|
||||
return blameType === BlameRequestType.CAR_BODY
|
||||
? fileReviewer.CarBodyExpertiseClaim
|
||||
: fileReviewer.ThirdPartyExpertiseClaim;
|
||||
}
|
||||
|
||||
function blameTypeLabel(blameType: BlameRequestType): string {
|
||||
return blameType === BlameRequestType.CAR_BODY
|
||||
? "car body (بدنه)"
|
||||
: "third party (ثالث)";
|
||||
}
|
||||
|
||||
export function assertFileMakerCanCreateBlameType(
|
||||
fileMaker: FileMakerFanavaranExpertCodes,
|
||||
blameType: BlameRequestType,
|
||||
): void {
|
||||
const raw = fileMakerStageOneExpertCode(fileMaker, blameType);
|
||||
if (parseFanavaranExpertId(raw) != null) {
|
||||
return;
|
||||
}
|
||||
throw new BadRequestException({
|
||||
code: "FILE_MAKER_FANAVARAN_EXPERT_CODE_MISSING",
|
||||
message: `Your FileMaker profile has no Fanavaran stage-1 claim expert id for ${blameTypeLabel(blameType)} files. You cannot create this file type.`,
|
||||
});
|
||||
}
|
||||
|
||||
export function assertFileReviewerCanReviewBlameType(
|
||||
fileReviewer: FileReviewerFanavaranExpertCodes,
|
||||
blameType: BlameRequestType,
|
||||
): void {
|
||||
const raw = fileReviewerExpertiseExpertCode(fileReviewer, blameType);
|
||||
if (parseFanavaranExpertId(raw) != null) {
|
||||
return;
|
||||
}
|
||||
throw new BadRequestException({
|
||||
code: "FILE_REVIEWER_FANAVARAN_EXPERT_CODE_MISSING",
|
||||
message: `Your FileReviewer profile has no Fanavaran expertise claim expert id for ${blameTypeLabel(blameType)} files. You cannot review this file type.`,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveFileMakerStageOneExpertId(
|
||||
fileMaker: FileMakerFanavaranExpertCodes,
|
||||
blameType: BlameRequestType,
|
||||
): number | null {
|
||||
return parseFanavaranExpertId(
|
||||
fileMakerStageOneExpertCode(fileMaker, blameType),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveFileReviewerExpertiseExpertId(
|
||||
fileReviewer: FileReviewerFanavaranExpertCodes,
|
||||
blameType: BlameRequestType,
|
||||
): number | null {
|
||||
return parseFanavaranExpertId(
|
||||
fileReviewerExpertiseExpertCode(fileReviewer, blameType),
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,26 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
|
||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/accident-causes`,
|
||||
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",
|
||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-report-type`,
|
||||
|
||||
@@ -277,6 +277,16 @@ export class FanavaranLookupService {
|
||||
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(
|
||||
clientKey: FanavaranClientKey,
|
||||
vehicleId: number,
|
||||
|
||||
@@ -36,6 +36,85 @@ export class LookupsController {
|
||||
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")
|
||||
@ApiOkResponse({
|
||||
description: "Returns accident report type lookup data",
|
||||
|
||||
@@ -106,6 +106,36 @@ export class LookupsService {
|
||||
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> {
|
||||
return await this.getClientRemoteLookup("accident-report-type");
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
||||
import { FanavaranLocationService } from "src/fanavaran/fanavaran-location.service";
|
||||
import {
|
||||
assertFileMakerCanCreateBlameType,
|
||||
assertFileReviewerCanReviewBlameType,
|
||||
} from "src/fanavaran/fanavaran-file-role-expert-ids";
|
||||
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
|
||||
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
||||
import { isOtpExpiryActive } from "src/helpers/user-otp-expiry";
|
||||
import { parseIranLocalDateTime } from "src/helpers/iran-datetime";
|
||||
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
||||
@@ -1088,6 +1094,8 @@ export class RequestManagementService {
|
||||
private readonly hashService: HashService,
|
||||
private readonly userAuthService: UserAuthService,
|
||||
private readonly fanavaranLocationService: FanavaranLocationService,
|
||||
private readonly fileMakerDbService: FileMakerDbService,
|
||||
private readonly fileReviewerDbService: FileReviewerDbService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -5325,6 +5333,14 @@ export class RequestManagementService {
|
||||
"This file has been taken by another FileReviewer.",
|
||||
);
|
||||
}
|
||||
const fileReviewer = await this.fileReviewerDbService.findById(
|
||||
String(expert.sub),
|
||||
);
|
||||
if (!fileReviewer) {
|
||||
throw new ForbiddenException("FileReviewer account not found.");
|
||||
}
|
||||
assertFileReviewerCanReviewBlameType(fileReviewer, req.type);
|
||||
|
||||
if (!assignedId) {
|
||||
// Atomically claim — ignore if another reviewer won the race (they would have
|
||||
// been caught by the assignedId check above on their own first call).
|
||||
@@ -5633,6 +5649,13 @@ export class RequestManagementService {
|
||||
}
|
||||
|
||||
const isFileMakerRole = (expert as any)?.role === RoleEnum.FILE_MAKER;
|
||||
if (isFileMakerRole) {
|
||||
const fileMaker = await this.fileMakerDbService.findById(String(expert.sub));
|
||||
if (!fileMaker) {
|
||||
throw new ForbiddenException("FileMaker account not found.");
|
||||
}
|
||||
assertFileMakerCanCreateBlameType(fileMaker, type);
|
||||
}
|
||||
const created = await this.blameRequestDbService.create({
|
||||
publicId,
|
||||
requestNo: publicId,
|
||||
|
||||
Reference in New Issue
Block a user