forked from Yara724/api
Route inquiries by participant role
This commit is contained in:
@@ -78,6 +78,7 @@ import {
|
||||
blameDamagedPartyMatchesUser,
|
||||
resolveClaimOwnerFieldsFromBlame,
|
||||
resolveClaimOwnerParty,
|
||||
resolveDamagedPartyRow,
|
||||
resolveDamagedPartyUserId,
|
||||
} from "src/helpers/blame-damaged-party";
|
||||
import { claimCaseInitiatedByFieldExpert } from "src/helpers/tenant-scope";
|
||||
@@ -115,6 +116,8 @@ import { BranchDbService } from "src/client/entities/db-service/branch.db.servic
|
||||
import { CreationMethod } from "src/request-management/entities/schema/request-management.schema";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
|
||||
import { participantForStoredPartyRole } from "src/request-management/inquiry-participant-resolver";
|
||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||
import {
|
||||
canFinalizeExpertResend,
|
||||
@@ -622,6 +625,20 @@ export class ClaimRequestManagementService {
|
||||
return this.parsePlateFromCompactString(compactPlateId);
|
||||
}
|
||||
|
||||
private resolveVehicleOwnerNationalCodeForClaim(
|
||||
blameRequest: any,
|
||||
): string | undefined {
|
||||
const damagedParty = resolveDamagedPartyRow(blameRequest as any);
|
||||
const owner = damagedParty
|
||||
? participantForStoredPartyRole(
|
||||
damagedParty as any,
|
||||
InquiryParticipantRole.VEHICLE_OWNER,
|
||||
)
|
||||
: undefined;
|
||||
const nationalCode = String(owner?.nationalCode ?? "").trim();
|
||||
return nationalCode || undefined;
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -9195,7 +9212,7 @@ export class ClaimRequestManagementService {
|
||||
|
||||
const shebaDigits = shebaInput.replace(/^IR/i, "");
|
||||
const shebaNumber = `IR${shebaDigits}`;
|
||||
const nationalCode = String(
|
||||
const submittedNationalCode = String(
|
||||
((body as any).nationalCodeOfOwner ??
|
||||
(body as any).nationalCodeOfInsurer ??
|
||||
"") as string,
|
||||
@@ -9205,17 +9222,28 @@ export class ClaimRequestManagementService {
|
||||
"shebaNumber is required and must be valid (IR + 24 digits or only 24 digits)",
|
||||
);
|
||||
}
|
||||
if (!/^[0-9]{10}$/.test(nationalCode)) {
|
||||
throw new BadRequestException(
|
||||
"nationalCodeOfOwner is required and must be exactly 10 digits",
|
||||
);
|
||||
}
|
||||
|
||||
const blameRequest = claimCase.blameRequestId
|
||||
? await this.blameRequestDbService.findById(
|
||||
claimCase.blameRequestId.toString(),
|
||||
)
|
||||
: null;
|
||||
const storedOwnerNationalCode =
|
||||
this.resolveVehicleOwnerNationalCodeForClaim(blameRequest);
|
||||
if (
|
||||
storedOwnerNationalCode &&
|
||||
submittedNationalCode &&
|
||||
submittedNationalCode !== storedOwnerNationalCode
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Submitted national code does not match the vehicle owner saved on the blame case.",
|
||||
);
|
||||
}
|
||||
const nationalCode = storedOwnerNationalCode ?? submittedNationalCode;
|
||||
if (!/^[0-9]{10}$/.test(nationalCode)) {
|
||||
throw new BadRequestException(
|
||||
"Vehicle owner national code is unavailable; a 10-digit nationalCodeOfOwner is required only for legacy claims.",
|
||||
);
|
||||
}
|
||||
const ownershipPlate = this.resolveOwnershipPlateForClaim(
|
||||
claimCase,
|
||||
blameRequest,
|
||||
|
||||
@@ -56,20 +56,21 @@ export class SelectOtherPartsV2Dto {
|
||||
@IsString()
|
||||
shebaNumber?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'National code of insurer/owner - 10 digits',
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Legacy fallback for claims created before participant roles were stored. New claims derive the vehicle owner national code from the linked blame case.',
|
||||
example: '1234567890',
|
||||
pattern: '^[0-9]{10}$',
|
||||
minLength: 10,
|
||||
maxLength: 10,
|
||||
})
|
||||
@IsNotEmpty({ message: 'nationalCodeOfInsurer is required' })
|
||||
@IsOptional()
|
||||
@IsString({ message: 'National code must be a string' })
|
||||
@Length(10, 10, { message: 'National code must be exactly 10 digits' })
|
||||
@Matches(/^[0-9]{10}$/, {
|
||||
message: 'National code must contain exactly 10 digits',
|
||||
})
|
||||
nationalCodeOfInsurer: string;
|
||||
nationalCodeOfInsurer?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Legacy alias for backward compatibility',
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
isMappedPolicyCurrent,
|
||||
normalizeInquirySubmission,
|
||||
participantForRole,
|
||||
participantForStoredPartyRole,
|
||||
resolveInquiryParticipants,
|
||||
resolveInquirySubjects,
|
||||
resolveInquiryVehicle,
|
||||
runPlateInquiryWithFallback,
|
||||
vehiclePlateCandidates,
|
||||
@@ -160,6 +162,62 @@ describe("inquiry participant resolver", () => {
|
||||
expect(normalized.vehicleOwner?.nationalCode).toBe("0012345678");
|
||||
});
|
||||
|
||||
it("routes each inquiry to its domain participant", () => {
|
||||
const normalized = normalizeInquirySubmission(BlameRequestType.CAR_BODY, {
|
||||
vehicle: {
|
||||
currentPlate: {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
},
|
||||
},
|
||||
driver: {
|
||||
nationalCode: "0011111111",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: false,
|
||||
},
|
||||
vehicleOwner: {
|
||||
nationalCode: "0022222222",
|
||||
birthday: "1365/02/02",
|
||||
},
|
||||
thirdPartyPolicyholder: {
|
||||
nationalCode: "0033333333",
|
||||
birthday: "1360/03/03",
|
||||
},
|
||||
carBodyPolicyholder: {
|
||||
nationalCode: "0044444444",
|
||||
birthday: "1355/04/04",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveInquirySubjects(normalized)).toEqual({
|
||||
thirdPartyPolicyNationalCode: "0033333333",
|
||||
carBodyPolicyNationalCode: "0044444444",
|
||||
shebaNationalCode: "0022222222",
|
||||
driverNationalCode: "0011111111",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves a persisted vehicle owner for later Sheba validation", () => {
|
||||
const party = {
|
||||
participants: [
|
||||
{ participantId: "DRIVER", nationalCode: "0011111111" },
|
||||
{ participantId: "VEHICLE_OWNER", nationalCode: "0022222222" },
|
||||
],
|
||||
participantRoles: {
|
||||
driver: "DRIVER",
|
||||
vehicleOwner: "VEHICLE_OWNER",
|
||||
thirdPartyPolicyholder: "DRIVER",
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
participantForStoredPartyRole(party, InquiryParticipantRole.VEHICLE_OWNER)
|
||||
?.nationalCode,
|
||||
).toBe("0022222222");
|
||||
});
|
||||
|
||||
it("orders the current plate before the previous-plate fallback", () => {
|
||||
const currentPlate = {
|
||||
leftDigits: "44",
|
||||
|
||||
@@ -58,6 +58,40 @@ export interface NormalizedInquirySubmission<T extends Record<string, any>> {
|
||||
vehicle?: ResolvedInquiryVehicle;
|
||||
}
|
||||
|
||||
export interface InquirySubjects {
|
||||
thirdPartyPolicyNationalCode: string;
|
||||
carBodyPolicyNationalCode?: string;
|
||||
shebaNationalCode: string;
|
||||
driverNationalCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The single routing seam for external inquiries. Policy checks belong to
|
||||
* their policyholder, Sheba belongs to the vehicle owner, and licence data
|
||||
* belongs to the driver even when those roles resolve to different people.
|
||||
*/
|
||||
export function resolveInquirySubjects(
|
||||
submission: NormalizedInquirySubmission<Record<string, any>>,
|
||||
): InquirySubjects {
|
||||
if (!submission.vehicleOwner) {
|
||||
throw new BadRequestException(
|
||||
"Vehicle owner identity is required for Sheba validation.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
thirdPartyPolicyNationalCode:
|
||||
submission.thirdPartyPolicyholder.nationalCode,
|
||||
...(submission.carBodyPolicyholder
|
||||
? {
|
||||
carBodyPolicyNationalCode:
|
||||
submission.carBodyPolicyholder.nationalCode,
|
||||
}
|
||||
: {}),
|
||||
shebaNationalCode: submission.vehicleOwner.nationalCode,
|
||||
driverNationalCode: submission.driver.nationalCode,
|
||||
};
|
||||
}
|
||||
|
||||
export type ResolvedInquiryVehicle = InquiryVehicleInputDto & {
|
||||
registrationState: VehicleRegistrationState;
|
||||
};
|
||||
@@ -75,6 +109,24 @@ export function participantForRole(
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve a participant from the role links persisted on a blame party. */
|
||||
export function participantForStoredPartyRole(
|
||||
party: {
|
||||
participants?: Array<Record<string, any>>;
|
||||
participantRoles?: Partial<InquiryParticipantRoleAssignments>;
|
||||
},
|
||||
role: InquiryParticipantRole,
|
||||
): Record<string, any> | undefined {
|
||||
const roleField = ROLE_FIELDS[
|
||||
role
|
||||
] as keyof InquiryParticipantRoleAssignments;
|
||||
const participantId = party.participantRoles?.[roleField];
|
||||
if (!participantId || !Array.isArray(party.participants)) return undefined;
|
||||
return party.participants.find(
|
||||
(participant) => participant?.participantId === participantId,
|
||||
);
|
||||
}
|
||||
|
||||
const ROLE_FIELDS: Record<
|
||||
InquiryParticipantRole,
|
||||
keyof InquiryParticipantFieldsDto
|
||||
|
||||
@@ -125,8 +125,10 @@ import { normalizePlateText } from "src/utils/plate-normalizer/plate-normalizer.
|
||||
import { fanavaranClaimReferences } from "src/claim-request-management/fanavaran-claim-references";
|
||||
import {
|
||||
isMappedPolicyCurrent,
|
||||
InquirySubjects,
|
||||
NormalizedInquirySubmission,
|
||||
normalizeInquirySubmission,
|
||||
resolveInquirySubjects,
|
||||
runPlateInquiryWithFallback,
|
||||
} from "./inquiry-participant-resolver";
|
||||
|
||||
@@ -361,6 +363,7 @@ export class RequestManagementService {
|
||||
attempts: [],
|
||||
};
|
||||
}
|
||||
const subjects = resolveInquirySubjects(submission);
|
||||
const result = await runPlateInquiryWithFallback({
|
||||
vehicle: submission.vehicle,
|
||||
fallbackCurrentPlate: submission.dto.plate,
|
||||
@@ -368,8 +371,7 @@ export class RequestManagementService {
|
||||
this.sandHubService.getTejaratBlockInquiry(
|
||||
{
|
||||
plate: plate as any,
|
||||
nationalCodeOfInsurer:
|
||||
submission.thirdPartyPolicyholder.nationalCode,
|
||||
nationalCodeOfInsurer: subjects.thirdPartyPolicyNationalCode,
|
||||
},
|
||||
options,
|
||||
),
|
||||
@@ -389,14 +391,19 @@ export class RequestManagementService {
|
||||
private async getCarBodyPlateInquiry(
|
||||
submission: NormalizedInquirySubmission<Record<string, any>>,
|
||||
): Promise<any> {
|
||||
const policyholder =
|
||||
submission.carBodyPolicyholder ?? submission.thirdPartyPolicyholder;
|
||||
const subjects = resolveInquirySubjects(submission);
|
||||
const policyholderNationalCode = subjects.carBodyPolicyNationalCode;
|
||||
if (!policyholderNationalCode) {
|
||||
throw new BadRequestException(
|
||||
"Car-body policyholder identity is required for a CAR_BODY inquiry.",
|
||||
);
|
||||
}
|
||||
const result = await runPlateInquiryWithFallback({
|
||||
vehicle: submission.vehicle,
|
||||
fallbackCurrentPlate: submission.dto.plate,
|
||||
query: (plate) =>
|
||||
this.sandHubService.getCarBodyInquiry({
|
||||
nationalCodeOfInsurer: policyholder.nationalCode,
|
||||
nationalCodeOfInsurer: policyholderNationalCode,
|
||||
plate: plate as any,
|
||||
}),
|
||||
isUsable: (value) =>
|
||||
@@ -416,6 +423,32 @@ export class RequestManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
private async getThirdPartyVinInquiry(
|
||||
submission: NormalizedInquirySubmission<Record<string, any>>,
|
||||
options?: Record<string, any>,
|
||||
): Promise<any> {
|
||||
const chassis = String(
|
||||
submission.vehicle?.vin ?? submission.dto.vin ?? "",
|
||||
).trim();
|
||||
if (!chassis) {
|
||||
throw new BadRequestException(
|
||||
"vehicle.vin is required for a VIN/chassis inquiry.",
|
||||
);
|
||||
}
|
||||
if (submission.thirdPartyPolicyholder.unknown) {
|
||||
return { raw: null, mapped: {}, skipped: true };
|
||||
}
|
||||
const subjects = resolveInquirySubjects(submission);
|
||||
const result = await this.sandHubService.getPolicyByChassisInquiry(
|
||||
{
|
||||
nationalCode: subjects.thirdPartyPolicyNationalCode,
|
||||
chassis,
|
||||
},
|
||||
options,
|
||||
);
|
||||
return { ...result, skipped: false };
|
||||
}
|
||||
|
||||
private async runParticipantPersonalInquiries(
|
||||
req: any,
|
||||
role: PartyRole,
|
||||
@@ -2153,7 +2186,10 @@ export class RequestManagementService {
|
||||
role,
|
||||
);
|
||||
body = inquirySubmission.dto as unknown as InitialFormVinDto;
|
||||
const subjects = resolveInquirySubjects(inquirySubmission);
|
||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||
const policyholderUnknown =
|
||||
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||
|
||||
// Validation: driver/insurer sameness rules (identical to plate path)
|
||||
if (body.driverIsInsurer === false) {
|
||||
@@ -2194,8 +2230,8 @@ export class RequestManagementService {
|
||||
let inquiryRaw: any;
|
||||
let inquiryMapped: any;
|
||||
try {
|
||||
const inquiry = await this.sandHubService.getPolicyByChassisInquiry(
|
||||
body.vin,
|
||||
const inquiry = await this.getThirdPartyVinInquiry(
|
||||
inquirySubmission,
|
||||
inquiryOptions,
|
||||
);
|
||||
inquiryRaw = inquiry.raw;
|
||||
@@ -2206,11 +2242,23 @@ export class RequestManagementService {
|
||||
this.logger.log(
|
||||
`[ESG] policyByChassis mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
|
||||
source: "ESG_VIN_INQUIRY",
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
});
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"thirdParty",
|
||||
role,
|
||||
!inquiry.skipped,
|
||||
{
|
||||
source: "ESG_VIN_INQUIRY",
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
...(inquiry.skipped
|
||||
? {
|
||||
skipped: true,
|
||||
reason: "Third-party policyholder is unknown",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`[ESG] policyByChassis failed for request=${req._id}: ${err?.message || err}`,
|
||||
@@ -2271,7 +2319,7 @@ export class RequestManagementService {
|
||||
|
||||
// Resolve insurer client from inquiry response
|
||||
const clientName = inquiryMapped?.CompanyName;
|
||||
if (!clientName) {
|
||||
if (!clientName && !policyholderUnknown) {
|
||||
const error = new BadRequestException(
|
||||
"CompanyName missing from VIN inquiry response",
|
||||
);
|
||||
@@ -2291,11 +2339,13 @@ export class RequestManagementService {
|
||||
throw error;
|
||||
}
|
||||
const companyCode = inquiryMapped?.CompanyCode;
|
||||
const client = await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
);
|
||||
if (!client) {
|
||||
const client = clientName
|
||||
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
)
|
||||
: null;
|
||||
if (clientName && !client) {
|
||||
const error = new BadRequestException(
|
||||
"CompanyCode missing or invalid in VIN inquiry response",
|
||||
);
|
||||
@@ -2380,9 +2430,7 @@ export class RequestManagementService {
|
||||
if (req.type === BlameRequestType.CAR_BODY && role === PartyRole.FIRST) {
|
||||
try {
|
||||
const carBodyInfo = await this.sandHubService.getCarBodyInquiry({
|
||||
nationalCodeOfInsurer:
|
||||
inquirySubmission.carBodyPolicyholder?.nationalCode ??
|
||||
body.nationalCodeOfInsurer,
|
||||
nationalCodeOfInsurer: subjects.carBodyPolicyNationalCode!,
|
||||
plate: body.vin,
|
||||
});
|
||||
this.recordPartyCaseInquiryStatus(req, "carBody", role, true, {
|
||||
@@ -9743,13 +9791,14 @@ export class RequestManagementService {
|
||||
partyData: RunInquiriesV3Dto,
|
||||
partyRole: PartyRole,
|
||||
party: any,
|
||||
): Promise<{ clientId?: string }> {
|
||||
): Promise<{ clientId?: string; subjects: InquirySubjects }> {
|
||||
const inquirySubmission = this.normalizeInquiryInput(
|
||||
req.type,
|
||||
partyData,
|
||||
partyRole,
|
||||
);
|
||||
partyData = inquirySubmission.dto as RunInquiriesV3Dto;
|
||||
const subjects = resolveInquirySubjects(inquirySubmission);
|
||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||
const policyholderUnknown =
|
||||
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||
@@ -10095,19 +10144,18 @@ export class RequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
return { clientId };
|
||||
return { clientId, subjects };
|
||||
}
|
||||
|
||||
private async validateShebaV3(
|
||||
partyData: RunInquiriesV3Dto,
|
||||
sheba: string | undefined,
|
||||
vehicleOwnerNationalCode: string,
|
||||
clientId?: string,
|
||||
): Promise<void> {
|
||||
if (!partyData.sheba) return;
|
||||
const nationalCode =
|
||||
partyData.nationalCodeOfInsurer || partyData.nationalCodeOfDriver;
|
||||
if (!sheba) return;
|
||||
await this.sandHubService.getShebaValidation(
|
||||
nationalCode,
|
||||
partyData.sheba,
|
||||
vehicleOwnerNationalCode,
|
||||
sheba,
|
||||
clientId ? { clientId } : undefined,
|
||||
);
|
||||
}
|
||||
@@ -10340,6 +10388,7 @@ export class RequestManagementService {
|
||||
private async syncV3ClaimDamagedPartyFromBlame(
|
||||
req: any,
|
||||
partyData: RunInquiriesV3Dto,
|
||||
vehicleOwnerNationalCode: string,
|
||||
): Promise<void> {
|
||||
const claim = await this.claimCaseDbService.findOne({
|
||||
blameRequestId: (req as any)._id,
|
||||
@@ -10358,9 +10407,6 @@ export class RequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
const nationalCode =
|
||||
partyData.nationalCodeOfInsurer || partyData.nationalCodeOfDriver || "";
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(String(claim._id), {
|
||||
$set: {
|
||||
damagedPartyUserId: new Types.ObjectId(ownerFields.userId),
|
||||
@@ -10375,7 +10421,7 @@ export class RequestManagementService {
|
||||
...(partyData.sheba
|
||||
? {
|
||||
"money.sheba": partyData.sheba,
|
||||
"money.nationalCodeOfInsurer": nationalCode,
|
||||
"money.nationalCodeOfInsurer": vehicleOwnerNationalCode,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
@@ -10405,9 +10451,6 @@ export class RequestManagementService {
|
||||
if (!partyRole) {
|
||||
throw new ConflictException("All party inquiries are already complete.");
|
||||
}
|
||||
dto = this.normalizeInquiryInput(req.type, dto, partyRole)
|
||||
.dto as RunInquiriesV3Dto;
|
||||
|
||||
if (partyRole === PartyRole.FIRST) {
|
||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstIdx === -1)
|
||||
@@ -10427,7 +10470,7 @@ export class RequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.runPartyInquiriesV3Internal(
|
||||
const inquiryResult = await this.runPartyInquiriesV3Internal(
|
||||
req,
|
||||
dto,
|
||||
PartyRole.FIRST,
|
||||
@@ -10435,7 +10478,8 @@ export class RequestManagementService {
|
||||
);
|
||||
if (req.type === BlameRequestType.CAR_BODY) {
|
||||
await this.validateShebaV3(
|
||||
dto,
|
||||
dto.sheba,
|
||||
inquiryResult.subjects.shebaNationalCode,
|
||||
firstParty.person?.clientId?.toString(),
|
||||
);
|
||||
}
|
||||
@@ -10443,12 +10487,12 @@ export class RequestManagementService {
|
||||
this.markPartyInquiriesCompleteOnBlame(req, PartyRole.FIRST, actor);
|
||||
await (req as any).save();
|
||||
|
||||
const nationalCode =
|
||||
dto.nationalCodeOfInsurer || dto.nationalCodeOfDriver || "";
|
||||
const newClaim = await this.createV3ClaimFromBlame(req, actor, {
|
||||
sheba: req.type === BlameRequestType.CAR_BODY ? dto.sheba : undefined,
|
||||
nationalCode:
|
||||
req.type === BlameRequestType.CAR_BODY ? nationalCode : undefined,
|
||||
req.type === BlameRequestType.CAR_BODY
|
||||
? inquiryResult.subjects.shebaNationalCode
|
||||
: undefined,
|
||||
interimThirdParty: req.type === BlameRequestType.THIRD_PARTY,
|
||||
});
|
||||
|
||||
@@ -10491,13 +10535,17 @@ export class RequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.runPartyInquiriesV3Internal(
|
||||
const inquiryResult = await this.runPartyInquiriesV3Internal(
|
||||
req,
|
||||
dto,
|
||||
PartyRole.SECOND,
|
||||
secondParty,
|
||||
);
|
||||
await this.validateShebaV3(dto, secondParty.person?.clientId?.toString());
|
||||
await this.validateShebaV3(
|
||||
dto.sheba,
|
||||
inquiryResult.subjects.shebaNationalCode,
|
||||
secondParty.person?.clientId?.toString(),
|
||||
);
|
||||
|
||||
this.ensureV3GuiltyPartyDecision(req);
|
||||
if (typeof (req as any).markModified === "function") {
|
||||
@@ -10505,7 +10553,11 @@ export class RequestManagementService {
|
||||
}
|
||||
await (req as any).save();
|
||||
|
||||
await this.syncV3ClaimDamagedPartyFromBlame(req, dto);
|
||||
await this.syncV3ClaimDamagedPartyFromBlame(
|
||||
req,
|
||||
dto,
|
||||
inquiryResult.subjects.shebaNationalCode,
|
||||
);
|
||||
|
||||
this.markPartyInquiriesCompleteOnBlame(req, PartyRole.SECOND, actor);
|
||||
await (req as any).save();
|
||||
@@ -10542,9 +10594,6 @@ export class RequestManagementService {
|
||||
if (!partyRole) {
|
||||
throw new ConflictException("All party inquiries are already complete.");
|
||||
}
|
||||
dto = this.normalizeInquiryInput(req.type, dto, partyRole)
|
||||
.dto as RunInquiriesVinV3Dto;
|
||||
|
||||
if (partyRole === PartyRole.FIRST) {
|
||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstIdx === -1)
|
||||
@@ -10564,7 +10613,7 @@ export class RequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.runPartyInquiriesVinV3Internal(
|
||||
const inquiryResult = await this.runPartyInquiriesVinV3Internal(
|
||||
req,
|
||||
dto,
|
||||
PartyRole.FIRST,
|
||||
@@ -10572,7 +10621,8 @@ export class RequestManagementService {
|
||||
);
|
||||
if (req.type === BlameRequestType.CAR_BODY) {
|
||||
await this.validateShebaV3(
|
||||
dto as any,
|
||||
dto.sheba,
|
||||
inquiryResult.subjects.shebaNationalCode,
|
||||
firstParty.person?.clientId?.toString(),
|
||||
);
|
||||
}
|
||||
@@ -10580,12 +10630,12 @@ export class RequestManagementService {
|
||||
this.markPartyInquiriesCompleteOnBlame(req, PartyRole.FIRST, actor);
|
||||
await (req as any).save();
|
||||
|
||||
const nationalCode =
|
||||
dto.nationalCodeOfInsurer || dto.nationalCodeOfDriver || "";
|
||||
const newClaim = await this.createV3ClaimFromBlame(req, actor, {
|
||||
sheba: req.type === BlameRequestType.CAR_BODY ? dto.sheba : undefined,
|
||||
nationalCode:
|
||||
req.type === BlameRequestType.CAR_BODY ? nationalCode : undefined,
|
||||
req.type === BlameRequestType.CAR_BODY
|
||||
? inquiryResult.subjects.shebaNationalCode
|
||||
: undefined,
|
||||
interimThirdParty: req.type === BlameRequestType.THIRD_PARTY,
|
||||
});
|
||||
|
||||
@@ -10628,14 +10678,15 @@ export class RequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.runPartyInquiriesVinV3Internal(
|
||||
const inquiryResult = await this.runPartyInquiriesVinV3Internal(
|
||||
req,
|
||||
dto,
|
||||
PartyRole.SECOND,
|
||||
secondParty,
|
||||
);
|
||||
await this.validateShebaV3(
|
||||
dto as any,
|
||||
dto.sheba,
|
||||
inquiryResult.subjects.shebaNationalCode,
|
||||
secondParty.person?.clientId?.toString(),
|
||||
);
|
||||
|
||||
@@ -10645,7 +10696,11 @@ export class RequestManagementService {
|
||||
}
|
||||
await (req as any).save();
|
||||
|
||||
await this.syncV3ClaimDamagedPartyFromBlame(req, dto as any);
|
||||
await this.syncV3ClaimDamagedPartyFromBlame(
|
||||
req,
|
||||
dto as any,
|
||||
inquiryResult.subjects.shebaNationalCode,
|
||||
);
|
||||
|
||||
this.markPartyInquiriesCompleteOnBlame(req, PartyRole.SECOND, actor);
|
||||
await (req as any).save();
|
||||
@@ -10669,14 +10724,17 @@ export class RequestManagementService {
|
||||
partyData: RunInquiriesVinV3Dto,
|
||||
partyRole: PartyRole,
|
||||
party: any,
|
||||
): Promise<{ clientId?: string }> {
|
||||
): Promise<{ clientId?: string; subjects: InquirySubjects }> {
|
||||
const inquirySubmission = this.normalizeInquiryInput(
|
||||
req.type,
|
||||
partyData,
|
||||
partyRole,
|
||||
);
|
||||
partyData = inquirySubmission.dto as RunInquiriesVinV3Dto;
|
||||
const subjects = resolveInquirySubjects(inquirySubmission);
|
||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||
const policyholderUnknown =
|
||||
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
|
||||
let clientId: string | undefined;
|
||||
const inquiryOptions = (cid?: string) =>
|
||||
@@ -10685,17 +10743,29 @@ export class RequestManagementService {
|
||||
let inquiryRaw: any;
|
||||
let inquiryMapped: any;
|
||||
try {
|
||||
const inquiry = await this.sandHubService.getPolicyByChassisInquiry(
|
||||
partyData.vin,
|
||||
const inquiry = await this.getThirdPartyVinInquiry(
|
||||
inquirySubmission,
|
||||
inquiryOptions(party?.person?.clientId?.toString()),
|
||||
);
|
||||
inquiryRaw = inquiry.raw;
|
||||
inquiryMapped = inquiry.mapped;
|
||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", partyRole, true, {
|
||||
source: "ESG_VIN_INQUIRY",
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
});
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"thirdParty",
|
||||
partyRole,
|
||||
!inquiry.skipped,
|
||||
{
|
||||
source: "ESG_VIN_INQUIRY",
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
...(inquiry.skipped
|
||||
? {
|
||||
skipped: true,
|
||||
reason: "Third-party policyholder is unknown",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`[V3-VIN] vin inquiry failed for ${roleLabel} party (request=${req._id}): ${err?.message || err}`,
|
||||
@@ -10737,7 +10807,7 @@ export class RequestManagementService {
|
||||
|
||||
const clientName = inquiryMapped?.CompanyName;
|
||||
const companyCode = inquiryMapped?.CompanyCode;
|
||||
if (!clientName) {
|
||||
if (!clientName && !policyholderUnknown) {
|
||||
const error = new BadRequestException(
|
||||
`CompanyName missing from ${roleLabel} party VIN inquiry response`,
|
||||
);
|
||||
@@ -10756,11 +10826,13 @@ export class RequestManagementService {
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw error;
|
||||
}
|
||||
const client = await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
);
|
||||
if (!client) {
|
||||
const client = clientName
|
||||
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
)
|
||||
: null;
|
||||
if (clientName && !client) {
|
||||
const error = new BadRequestException(
|
||||
`CompanyCode missing or invalid in ${roleLabel} party VIN inquiry response`,
|
||||
);
|
||||
@@ -10846,9 +10918,7 @@ export class RequestManagementService {
|
||||
) {
|
||||
try {
|
||||
const carBodyInfo = await this.sandHubService.getCarBodyInquiry({
|
||||
nationalCodeOfInsurer:
|
||||
inquirySubmission.carBodyPolicyholder?.nationalCode ??
|
||||
partyData.nationalCodeOfInsurer,
|
||||
nationalCodeOfInsurer: subjects.carBodyPolicyNationalCode!,
|
||||
plate: partyData.vin as any, // VIN used as identifier for CAR_BODY
|
||||
});
|
||||
this.recordPartyCaseInquiryStatus(req, "carBody", partyRole, true, {
|
||||
@@ -10996,7 +11066,7 @@ export class RequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
return { clientId };
|
||||
return { clientId, subjects };
|
||||
}
|
||||
|
||||
private assertBlameV3PartyDetailPhase(req: any, partyRole: PartyRole): void {
|
||||
@@ -12185,8 +12255,6 @@ export class RequestManagementService {
|
||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||
const firstParty = req.parties[firstIdx];
|
||||
|
||||
dto = this.normalizeInquiryInput(req.type, dto, PartyRole.FIRST).dto;
|
||||
|
||||
await this.runPartyInquiriesV3Internal(
|
||||
req,
|
||||
dto,
|
||||
@@ -12308,8 +12376,6 @@ export class RequestManagementService {
|
||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||
const firstParty = req.parties[firstIdx];
|
||||
|
||||
dto = this.normalizeInquiryInput(req.type, dto, PartyRole.FIRST).dto;
|
||||
|
||||
await this.runPartyInquiriesVinV3Internal(
|
||||
req,
|
||||
dto as RunInquiriesVinV3Dto,
|
||||
|
||||
@@ -176,12 +176,39 @@ describe("SandHubService inquiry mocks", () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.getPolicyByChassisInquiry("NAAR03HFFRDE07024", {
|
||||
enforceDeploymentClientMatch: true,
|
||||
}),
|
||||
service.getPolicyByChassisInquiry(
|
||||
{
|
||||
nationalCode: "1234567890",
|
||||
chassis: "NAAR03HFFRDE07024",
|
||||
},
|
||||
{ enforceDeploymentClientMatch: true },
|
||||
),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it("sends the policyholder national code with the VIN inquiry", async () => {
|
||||
externalInquirySettings.isInquiryLive.mockResolvedValue(true);
|
||||
const esg = jest.spyOn(service as any, "makeEsgRequest").mockResolvedValue({
|
||||
success: true,
|
||||
data: { CmpCod: "8", CmpNam: "بیمه پارسیان" },
|
||||
});
|
||||
|
||||
await service.getPolicyByChassisInquiry({
|
||||
nationalCode: "0012345678",
|
||||
chassis: "NAAR03HFFRDE07024",
|
||||
});
|
||||
|
||||
expect(esg).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/inquiry/policyByChassis"),
|
||||
{
|
||||
nationalCode: "0012345678",
|
||||
chassis: "NAAR03HFFRDE07024",
|
||||
},
|
||||
"vinChassis",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not accept a policy when CLIENT_ID is not configured", async () => {
|
||||
delete process.env.CLIENT_ID;
|
||||
externalInquirySettings.isInquiryLive.mockResolvedValue(true);
|
||||
@@ -295,6 +322,47 @@ describe("SandHubService inquiry mocks", () => {
|
||||
expect(result.source).toBe("ESG_CAR_BODY_VIN_INQUIRY");
|
||||
});
|
||||
|
||||
it("uses the tenant Fanavaran VIN lookup for non-Parsian car-body inquiries", async () => {
|
||||
process.env.CLIENT_ID = "15";
|
||||
process.env.FANAVARAN_CLIENT = "tejaratno";
|
||||
externalInquirySettings.isInquiryLive.mockResolvedValue(true);
|
||||
lookupsService.findLastProcessedCarPolicy.mockResolvedValue({
|
||||
policy: { CINumber: "BODY-15" },
|
||||
customer: {},
|
||||
vehicle: { VIN: "NAAR03HFFRDE07024" },
|
||||
});
|
||||
const tejarat = jest.spyOn(service, "getTejaratCarBodyInquiry");
|
||||
|
||||
const result = await service.getCarBodyInquiry({
|
||||
nationalCodeOfInsurer: "0012345678",
|
||||
plate: "NAAR03HFFRDE07024",
|
||||
});
|
||||
|
||||
expect(lookupsService.findLastProcessedCarPolicy).toHaveBeenCalledWith(
|
||||
"car-body",
|
||||
{ nationalCode: "0012345678", vin: "NAAR03HFFRDE07024" },
|
||||
);
|
||||
expect(tejarat).not.toHaveBeenCalled();
|
||||
expect(result.source).toBe("TEJARAT_CAR_BODY_VIN_INQUIRY");
|
||||
});
|
||||
|
||||
it("returns a VIN-shaped car-body mock without invoking a plate adapter", async () => {
|
||||
process.env.CLIENT_ID = "15";
|
||||
process.env.FANAVARAN_CLIENT = "tejaratno";
|
||||
const tejarat = jest.spyOn(service, "getTejaratCarBodyInquiry");
|
||||
|
||||
const result = await service.getCarBodyInquiry({
|
||||
nationalCodeOfInsurer: "0012345678",
|
||||
plate: "NAAR03HFFRDE07024",
|
||||
});
|
||||
|
||||
expect(tejarat).not.toHaveBeenCalled();
|
||||
expect(lookupsService.findLastProcessedCarPolicy).not.toHaveBeenCalled();
|
||||
expect(result.source).toBe("TEJARAT_CAR_BODY_VIN_INQUIRY");
|
||||
expect(result.mapped.VinNumberField).toBe("NAAR03HFFRDE07024");
|
||||
expect(result.mapped.insurerNationalCode).toBe("0012345678");
|
||||
});
|
||||
|
||||
it("uses car-body mock shape in Tejarat helper when inquiry is off", async () => {
|
||||
const raw = await (service as any).makeTejaratRequest(
|
||||
"http://example/block-inquiry-tejarat/badane",
|
||||
|
||||
@@ -250,11 +250,16 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
private buildMockCarBodyInquiryRaw(
|
||||
userDetail: SandHubDetailDto,
|
||||
userDetail: CarBodyInquiryDetail,
|
||||
ctx: MockInquiryCompanyContext,
|
||||
): Record<string, unknown> {
|
||||
const companyId = Number(ctx.companyId);
|
||||
const plate = userDetail?.plate;
|
||||
const plate =
|
||||
typeof userDetail?.plate === "string" ? undefined : userDetail?.plate;
|
||||
const vin =
|
||||
typeof userDetail?.plate === "string"
|
||||
? userDetail.plate
|
||||
: "LFP8C7PC3R1K12157";
|
||||
const platePartOne = Number(plate?.leftDigits ?? 16);
|
||||
const platePartThree = Number(plate?.centerDigits ?? 498);
|
||||
const plateSerialNumber = Number(plate?.ir ?? 60);
|
||||
@@ -272,8 +277,8 @@ export class SandHubService {
|
||||
issueDate: "1405/01/16",
|
||||
hasEndorsement: null,
|
||||
motorNumber: "TZ196XYAP223A210074",
|
||||
chassisNumber: "LFP8C7PC3R1K12157",
|
||||
vin: "LFP8C7PC3R1K12157",
|
||||
chassisNumber: vin,
|
||||
vin,
|
||||
plateTypeId: 9,
|
||||
plateTypeTitle: "پلاک قدیمی",
|
||||
platePartOne,
|
||||
@@ -933,28 +938,48 @@ export class SandHubService {
|
||||
}> {
|
||||
const useParsianCarBodyLookup = this.shouldUseParsianCarBodyLookup();
|
||||
const live = await this.isInquiryLive("carBodyPlate", options);
|
||||
const plateOrVin = userDetail.plate;
|
||||
const isVinInquiry = typeof plateOrVin === "string";
|
||||
|
||||
if (!live) {
|
||||
// Mock results are generated for the active deployment. They must not be
|
||||
// treated as evidence of a real insurer and therefore do not go through
|
||||
// the live-policy insurer gate.
|
||||
const result = await this.getTejaratCarBodyInquiry(
|
||||
userDetail as SandHubDetailDto,
|
||||
options,
|
||||
);
|
||||
const ctx = await this.mockCompanyContext(options);
|
||||
const raw = this.buildMockCarBodyInquiryRaw(userDetail, ctx);
|
||||
const result = { raw, mapped: this.mapCarBodyInquiryResponse(raw) };
|
||||
return {
|
||||
source:
|
||||
typeof userDetail.plate === "string"
|
||||
? useParsianCarBodyLookup
|
||||
? "ESG_CAR_BODY_VIN_INQUIRY"
|
||||
: "TEJARAT_CAR_BODY_VIN_INQUIRY"
|
||||
: useParsianCarBodyLookup
|
||||
? "ESG_CAR_BODY_INQUIRY"
|
||||
: "TEJARAT_CAR_BODY_INQUIRY",
|
||||
source: isVinInquiry
|
||||
? useParsianCarBodyLookup
|
||||
? "ESG_CAR_BODY_VIN_INQUIRY"
|
||||
: "TEJARAT_CAR_BODY_VIN_INQUIRY"
|
||||
: useParsianCarBodyLookup
|
||||
? "ESG_CAR_BODY_INQUIRY"
|
||||
: "TEJARAT_CAR_BODY_INQUIRY",
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
if (isVinInquiry) {
|
||||
const raw = await this.lookupsService.findLastProcessedCarPolicy(
|
||||
"car-body",
|
||||
{
|
||||
nationalCode: String(userDetail.nationalCodeOfInsurer),
|
||||
vin: plateOrVin,
|
||||
},
|
||||
);
|
||||
if (useParsianCarBodyLookup) {
|
||||
this.assertParsianCarBodyLookupMatchesDeployment();
|
||||
}
|
||||
return {
|
||||
source: useParsianCarBodyLookup
|
||||
? "ESG_CAR_BODY_VIN_INQUIRY"
|
||||
: "TEJARAT_CAR_BODY_VIN_INQUIRY",
|
||||
raw,
|
||||
mapped: mapEsgCarBodyPolicyToInquiry(raw),
|
||||
};
|
||||
}
|
||||
|
||||
if (!useParsianCarBodyLookup) {
|
||||
const result = await this.getTejaratCarBodyInquiry(
|
||||
userDetail as SandHubDetailDto,
|
||||
@@ -962,28 +987,18 @@ export class SandHubService {
|
||||
);
|
||||
this.assertInsuranceMatchesDeployment(result.mapped, "CAR_BODY");
|
||||
return {
|
||||
source:
|
||||
typeof userDetail.plate === "string"
|
||||
? "TEJARAT_CAR_BODY_VIN_INQUIRY"
|
||||
: "TEJARAT_CAR_BODY_INQUIRY",
|
||||
source: "TEJARAT_CAR_BODY_INQUIRY",
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
const plateOrVin = userDetail.plate;
|
||||
const query =
|
||||
typeof plateOrVin === "string"
|
||||
? {
|
||||
nationalCode: String(userDetail.nationalCodeOfInsurer),
|
||||
vin: plateOrVin,
|
||||
}
|
||||
: {
|
||||
nationalCode: String(userDetail.nationalCodeOfInsurer),
|
||||
plaqueLeft: String(plateOrVin.leftDigits),
|
||||
plaqueLetter: String(plateOrVin.centerAlphabet),
|
||||
plaqueRight: String(plateOrVin.centerDigits),
|
||||
plaqueSerial: String(plateOrVin.ir),
|
||||
};
|
||||
const query = {
|
||||
nationalCode: String(userDetail.nationalCodeOfInsurer),
|
||||
plaqueLeft: String(plateOrVin.leftDigits),
|
||||
plaqueLetter: String(plateOrVin.centerAlphabet),
|
||||
plaqueRight: String(plateOrVin.centerDigits),
|
||||
plaqueSerial: String(plateOrVin.ir),
|
||||
};
|
||||
const raw = await this.lookupsService.findLastProcessedCarPolicy(
|
||||
"car-body",
|
||||
query,
|
||||
@@ -991,10 +1006,7 @@ export class SandHubService {
|
||||
this.assertParsianCarBodyLookupMatchesDeployment();
|
||||
|
||||
return {
|
||||
source:
|
||||
typeof plateOrVin === "string"
|
||||
? "ESG_CAR_BODY_VIN_INQUIRY"
|
||||
: "ESG_CAR_BODY_INQUIRY",
|
||||
source: "ESG_CAR_BODY_INQUIRY",
|
||||
raw,
|
||||
mapped: mapEsgCarBodyPolicyToInquiry(raw),
|
||||
};
|
||||
@@ -1109,16 +1121,19 @@ export class SandHubService {
|
||||
* `buildMockPlateInquiryRaw` so the downstream mapper (`mapEsgPolicyByPlateToOldFormat`)
|
||||
* can normalise it into the same `{ raw, mapped }` contract used by the plate path.
|
||||
*
|
||||
* @param chassisNo - 17-character VIN / chassis number
|
||||
* @param identity - policyholder national code and 17-character VIN/chassis
|
||||
* @param options - optional per-tenant client scope
|
||||
*/
|
||||
async getPolicyByChassisInquiry(
|
||||
chassisNo: string,
|
||||
identity: { nationalCode: string; chassis: string },
|
||||
options?: SandHubInquiryOptions,
|
||||
): Promise<{ raw: any; mapped: any }> {
|
||||
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
||||
const requestUrl = `${baseUrl}/inquiry/policyByChassis`;
|
||||
const requestPayload = { chassisNo };
|
||||
const requestPayload = {
|
||||
nationalCode: String(identity.nationalCode),
|
||||
chassis: String(identity.chassis),
|
||||
};
|
||||
|
||||
const live = await this.isInquiryLive("vinChassis", options);
|
||||
|
||||
@@ -1126,7 +1141,7 @@ export class SandHubService {
|
||||
const ctx = await this.mockCompanyContext(options);
|
||||
const raw = this.buildMockPlateInquiryRaw(ctx);
|
||||
this.logger.debug(
|
||||
`[MOCK] getPolicyByChassisInquiry chassisNo=${chassisNo}`,
|
||||
`[MOCK] getPolicyByChassisInquiry nationalCode=${identity.nationalCode} chassis=${identity.chassis}`,
|
||||
);
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw);
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
|
||||
Reference in New Issue
Block a user