Harden inquiry participant edge cases

This commit is contained in:
SepehrYahyaee
2026-09-13 11:18:37 +03:30
parent c64f23091a
commit d060b6d9e2
11 changed files with 505 additions and 84 deletions

View File

@@ -124,6 +124,7 @@ import { resolveLinkedUserIdStrings } from "src/helpers/user-access-resolver";
import { normalizePlateText } from "src/utils/plate-normalizer/plate-normalizer.service";
import { fanavaranClaimReferences } from "src/claim-request-management/fanavaran-claim-references";
import {
isMappedPolicyCurrent,
NormalizedInquirySubmission,
normalizeInquirySubmission,
runPlateInquiryWithFallback,
@@ -308,8 +309,12 @@ export class RequestManagementService {
private normalizeInquiryInput<T extends Record<string, any>>(
type: BlameRequestType,
input: T,
partyRole?: PartyRole,
): NormalizedInquirySubmission<T> {
return normalizeInquirySubmission(type, input);
return normalizeInquirySubmission(type, input, {
allowUnknownThirdPartyPolicyholder:
type === BlameRequestType.THIRD_PARTY && partyRole === PartyRole.SECOND,
});
}
private applyInquiryParticipantsToParty(
@@ -335,6 +340,15 @@ export class RequestManagementService {
submission: NormalizedInquirySubmission<Record<string, any>>,
options?: Record<string, any>,
): Promise<any> {
if (submission.thirdPartyPolicyholder.unknown) {
return {
raw: null,
mapped: {},
skipped: true,
plateKind: "CURRENT",
attempts: [],
};
}
const result = await runPlateInquiryWithFallback({
vehicle: submission.vehicle,
fallbackCurrentPlate: submission.dto.plate,
@@ -348,7 +362,9 @@ export class RequestManagementService {
options,
),
isUsable: (value) =>
!value?.mapped?.Error && !!value?.mapped?.CompanyName,
!value?.mapped?.Error &&
!!value?.mapped?.CompanyName &&
isMappedPolicyCurrent(value.mapped),
mappedValue: (value) => value?.mapped ?? {},
});
return {
@@ -373,6 +389,7 @@ export class RequestManagementService {
}),
isUsable: (value) =>
!!value?.mapped &&
isMappedPolicyCurrent(value.mapped) &&
!!(
value.mapped.policyNumber ||
value.mapped.CompanyName ||
@@ -395,7 +412,9 @@ export class RequestManagementService {
): Promise<void> {
const participants = submission.participants.legacy
? [submission.thirdPartyPolicyholder]
: submission.participants.participants;
: submission.participants.participants.filter(
(participant) => !participant.unknown,
);
const results: Record<string, unknown> = {};
for (const participant of participants) {
@@ -535,6 +554,7 @@ export class RequestManagementService {
message: err?.message || String(err),
status: err?.response?.status,
data: err?.response?.data,
...(Array.isArray(err?.attempts) ? { attempts: err.attempts } : {}),
};
}
@@ -1575,9 +1595,15 @@ export class RequestManagementService {
: undefined;
}
const inquirySubmission = this.normalizeInquiryInput(req.type, body);
const inquirySubmission = this.normalizeInquiryInput(
req.type,
body,
role,
);
body = inquirySubmission.dto as unknown as AddPlateDto;
this.applyInquiryParticipantsToParty(party, inquirySubmission);
const policyholderUnknown =
inquirySubmission.thirdPartyPolicyholder.unknown === true;
// Validation: driver/insurer sameness rules
if (body.driverIsInsurer === false) {
@@ -1637,13 +1663,25 @@ export class RequestManagementService {
this.logger.log(
`[TEJARAT] block inquiry mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
);
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
source: inquirySource,
plateKind: inquiry.plateKind,
attempts: inquiry.attempts,
raw: inquiryRaw,
mapped: inquiryMapped,
});
this.recordPartyCaseInquiryStatus(
req,
"thirdParty",
role,
!inquiry.skipped,
{
source: inquirySource,
plateKind: inquiry.plateKind,
attempts: inquiry.attempts,
raw: inquiryRaw,
mapped: inquiryMapped,
...(inquiry.skipped
? {
skipped: true,
reason: "Third-party policyholder is unknown",
}
: {}),
},
);
if (inquiry.offline?.fanavaranDriverId != null) {
if (!party.person) party.person = {} as any;
party.person.fanavaranDriverId = inquiry.offline.fanavaranDriverId;
@@ -1714,17 +1752,19 @@ export class RequestManagementService {
// Find client by company code
const clientName = inquiryMapped?.CompanyName;
if (!clientName) {
if (!clientName && !policyholderUnknown) {
throw new BadRequestException(
`CompanyName missing from inquiry response`,
);
}
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) {
throw new BadRequestException(
`CompanyCode missing or invalid in inquiry response`,
);
@@ -1734,7 +1774,7 @@ export class RequestManagementService {
if (!party.person) party.person = {} as any;
const resolvedClientId =
(client as any)?._id ?? (client as any)?._doc?._id;
party.person.clientId = resolvedClientId;
if (resolvedClientId) party.person.clientId = resolvedClientId;
party.person.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
party.person.insurerLicense = body.insurerLicense;
@@ -1980,7 +2020,11 @@ export class RequestManagementService {
: undefined;
}
const inquirySubmission = this.normalizeInquiryInput(req.type, body);
const inquirySubmission = this.normalizeInquiryInput(
req.type,
body,
role,
);
body = inquirySubmission.dto as unknown as InitialFormVinDto;
this.applyInquiryParticipantsToParty(party, inquirySubmission);
@@ -2123,7 +2167,7 @@ export class RequestManagementService {
// Persist party data
const resolvedClientId =
(client as any)?._id ?? (client as any)?._doc?._id;
party.person.clientId = resolvedClientId;
if (resolvedClientId) party.person.clientId = resolvedClientId;
party.person.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
party.person.insurerLicense = body.insurerLicense;
@@ -6578,6 +6622,7 @@ export class RequestManagementService {
const inquirySubmission = this.normalizeInquiryInput(
req.type,
formData.firstPartyPlate,
PartyRole.FIRST,
);
const firstPartyPlate = inquirySubmission.dto;
let sandHubReport: any;
@@ -6586,8 +6631,7 @@ export class RequestManagementService {
? await (async () => {
const response = await this.sandHubService.getSandHubResponse({
plate: firstPartyPlate.plate,
nationalCodeOfInsurer:
firstPartyPlate.nationalCodeOfInsurer,
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
});
const mapped = response["_doc"] || response;
return {
@@ -6857,8 +6901,14 @@ export class RequestManagementService {
desc: string,
role: PartyRole,
) => {
const inquirySubmission = this.normalizeInquiryInput(req.type, plateDto);
const inquirySubmission = this.normalizeInquiryInput(
req.type,
plateDto,
role,
);
plateDto = inquirySubmission.dto;
const policyholderUnknown =
inquirySubmission.thirdPartyPolicyholder.unknown === true;
let inquiry: any;
try {
if (inquirySubmission.participants.legacy) {
@@ -6871,9 +6921,7 @@ export class RequestManagementService {
raw: mapped,
mapped,
plateKind: "CURRENT",
attempts: [
{ plateKind: "CURRENT", succeeded: true, usable: true },
],
attempts: [{ plateKind: "CURRENT", succeeded: true, usable: true }],
};
} else {
inquiry = await this.getThirdPartyPlateInquiry(inquirySubmission);
@@ -6897,23 +6945,37 @@ export class RequestManagementService {
"THIRD_PARTY",
);
}
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
this.recordPartyCaseInquiryStatus(
req,
"thirdParty",
role,
!inquiry.skipped,
{
source: inquiry.offline?.source ?? "TEJARAT_BLOCK_INQUIRY",
plateKind: inquiry.plateKind,
attempts: inquiry.attempts,
raw: inquiry.raw,
mapped: inquiry.mapped,
});
raw: inquiry.raw,
mapped: inquiry.mapped,
...(inquiry.skipped
? {
skipped: true,
reason: "Third-party policyholder is unknown",
}
: {}),
},
);
const clientName =
sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
const companyCode = sandHubReport?.CompanyCode;
const client = companyCode
? await this.clientService.findOrCreateClientByCompanyCode(
companyCode,
clientName,
)
: await this.clientService.findOne({ clientName });
if (!client) {
const client = clientName
? companyCode
? await this.clientService.findOrCreateClientByCompanyCode(
companyCode,
clientName,
)
: await this.clientService.findOne({ clientName })
: null;
if (!client && !policyholderUnknown) {
throw new NotFoundException(
`Client not found for company: ${clientName}`,
);
@@ -9112,9 +9174,15 @@ export class RequestManagementService {
partyRole: PartyRole,
party: any,
): Promise<{ clientId?: string }> {
const inquirySubmission = this.normalizeInquiryInput(req.type, partyData);
const inquirySubmission = this.normalizeInquiryInput(
req.type,
partyData,
partyRole,
);
partyData = inquirySubmission.dto as RunInquiriesV3Dto;
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) =>
@@ -9137,13 +9205,25 @@ export class RequestManagementService {
if (inquiry.offline?.source) {
inquirySource = inquiry.offline.source;
}
this.recordPartyCaseInquiryStatus(req, "thirdParty", partyRole, true, {
source: inquirySource,
plateKind: inquiry.plateKind,
attempts: inquiry.attempts,
raw: inquiryRaw,
mapped: inquiryMapped,
});
this.recordPartyCaseInquiryStatus(
req,
"thirdParty",
partyRole,
!inquiry.skipped,
{
source: inquirySource,
plateKind: inquiry.plateKind,
attempts: inquiry.attempts,
raw: inquiryRaw,
mapped: inquiryMapped,
...(inquiry.skipped
? {
skipped: true,
reason: "Third-party policyholder is unknown",
}
: {}),
},
);
if (inquiry.offline?.fanavaranDriverId != null) {
if (!party.person) party.person = {} as any;
party.person.fanavaranDriverId = inquiry.offline.fanavaranDriverId;
@@ -9174,16 +9254,18 @@ export class RequestManagementService {
const clientName = inquiryMapped?.CompanyName;
const companyCode = inquiryMapped?.CompanyCode;
if (!clientName) {
if (!clientName && !policyholderUnknown) {
throw new BadRequestException(
`CompanyName missing from ${roleLabel} party inquiry response`,
);
}
const client = await this.clientService.findOrCreateClientByCompanyCode(
companyCode,
clientName,
);
if (!client) {
const client = clientName
? await this.clientService.findOrCreateClientByCompanyCode(
companyCode,
clientName,
)
: null;
if (clientName && !client) {
throw new BadRequestException(
`CompanyCode missing or invalid in ${roleLabel} party inquiry response`,
);
@@ -9192,7 +9274,7 @@ export class RequestManagementService {
clientId = resolvedClientId ? String(resolvedClientId) : undefined;
if (!party.person) party.person = {} as any;
party.person.clientId = resolvedClientId;
if (resolvedClientId) party.person.clientId = resolvedClientId;
party.person.nationalCodeOfInsurer = partyData.nationalCodeOfInsurer;
party.person.nationalCodeOfDriver = partyData.nationalCodeOfDriver;
party.person.driverIsInsurer = partyData.driverIsInsurer;
@@ -9337,7 +9419,22 @@ export class RequestManagementService {
? partyData.insurerLicense
: partyData.driverLicense;
if (!licenseNumber) {
if (
!inquirySubmission.participants.legacy &&
inquirySubmission.driver.hasDrivingLicense === false
) {
this.recordPartyCaseInquiryStatus(
req,
"drivingLicence",
partyRole,
true,
{
participantId: inquirySubmission.driver.participantId,
skipped: true,
reason: "Driver declared no driving licence",
},
);
} else if (!licenseNumber) {
this.recordPartyCaseInquiryStatus(
req,
"drivingLicence",
@@ -9683,12 +9780,12 @@ export class RequestManagementService {
if (!req) throw new NotFoundException("Blame request not found");
this.assertBlameV3ExpertInPerson(req);
await this.verifyExpertAccessForBlameV2(req, actor);
dto = this.normalizeInquiryInput(req.type, dto).dto as RunInquiriesV3Dto;
const partyRole = this.resolveV3InquiryPartyRole(req);
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);
@@ -9820,12 +9917,12 @@ export class RequestManagementService {
if (!req) throw new NotFoundException("Blame request not found");
this.assertBlameV3ExpertInPerson(req);
await this.verifyExpertAccessForBlameV2(req, actor);
dto = this.normalizeInquiryInput(req.type, dto).dto as RunInquiriesVinV3Dto;
const partyRole = this.resolveV3InquiryPartyRole(req);
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);
@@ -9952,7 +10049,11 @@ export class RequestManagementService {
partyRole: PartyRole,
party: any,
): Promise<{ clientId?: string }> {
const inquirySubmission = this.normalizeInquiryInput(req.type, partyData);
const inquirySubmission = this.normalizeInquiryInput(
req.type,
partyData,
partyRole,
);
partyData = inquirySubmission.dto as RunInquiriesVinV3Dto;
this.applyInquiryParticipantsToParty(party, inquirySubmission);
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
@@ -10018,7 +10119,7 @@ export class RequestManagementService {
clientId = resolvedClientId ? String(resolvedClientId) : undefined;
if (!party.person) party.person = {} as any;
party.person.clientId = resolvedClientId;
if (resolvedClientId) party.person.clientId = resolvedClientId;
party.person.nationalCodeOfInsurer = partyData.nationalCodeOfInsurer;
party.person.nationalCodeOfDriver = partyData.nationalCodeOfDriver;
party.person.driverIsInsurer = partyData.driverIsInsurer;
@@ -10174,7 +10275,22 @@ export class RequestManagementService {
? partyData.insurerLicense
: partyData.driverLicense;
if (!licenseNumber) {
if (
!inquirySubmission.participants.legacy &&
inquirySubmission.driver.hasDrivingLicense === false
) {
this.recordPartyCaseInquiryStatus(
req,
"drivingLicence",
partyRole,
true,
{
participantId: inquirySubmission.driver.participantId,
skipped: true,
reason: "Driver declared no driving licence",
},
);
} else if (!licenseNumber) {
this.recordPartyCaseInquiryStatus(
req,
"drivingLicence",
@@ -11403,7 +11519,7 @@ export class RequestManagementService {
if (firstIdx === -1) throw new BadRequestException("First party not found");
const firstParty = req.parties[firstIdx];
dto = this.normalizeInquiryInput(req.type, dto).dto;
dto = this.normalizeInquiryInput(req.type, dto, PartyRole.FIRST).dto;
await this.runPartyInquiriesV3Internal(
req,
@@ -11526,7 +11642,7 @@ export class RequestManagementService {
if (firstIdx === -1) throw new BadRequestException("First party not found");
const firstParty = req.parties[firstIdx];
dto = this.normalizeInquiryInput(req.type, dto).dto;
dto = this.normalizeInquiryInput(req.type, dto, PartyRole.FIRST).dto;
await this.runPartyInquiriesVinV3Internal(
req,