Merge pull request 'Fixed Participants' (#314) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#314
This commit is contained in:
2026-09-14 14:27:49 +03:30
13 changed files with 518 additions and 199 deletions

View File

@@ -4,6 +4,7 @@ import {
} from "src/helpers/blame-damaged-party";
import { toJalaliDateAndTime } from "src/helpers/date-jalali";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { sanitizeStoredInquiryParticipants } from "src/request-management/inquiry-participant-resolver";
import {
InsurerFileReportField,
InsurerFileReportSection,
@@ -611,9 +612,8 @@ function buildParticipantRolesSection(
title: string,
party: ReportParty | null | undefined,
): InsurerFileReportSection | undefined {
const participants = Array.isArray(party?.participants)
? party.participants
: [];
const participants =
sanitizeStoredInquiryParticipants(party?.participants) ?? [];
const assignments = party?.participantRoles;
if (!assignments || participants.length === 0) return undefined;
@@ -627,22 +627,20 @@ function buildParticipantRolesSection(
const participant = participants.find(
(candidate) => String(candidate.participantId) === String(participantId),
);
const value = participant?.unknown
? PR.unknown
: [
asString(participant?.fullName),
participant?.nationalCode
? `${PR.nationalCode}: ${asString(participant.nationalCode)}`
: undefined,
participant?.birthday
? `${PR.birthDate}: ${formatBirthDate(participant.birthday)}`
: undefined,
participant?.licenseNumber
? `${PR.licenseNumber}: ${asString(participant.licenseNumber)}`
: undefined,
]
.filter(Boolean)
.join("، ");
const value = [
asString(participant?.fullName),
participant?.nationalCode
? `${PR.nationalCode}: ${asString(participant.nationalCode)}`
: undefined,
participant?.birthday
? `${PR.birthDate}: ${formatBirthDate(participant.birthday)}`
: undefined,
participant?.licenseNumber
? `${PR.licenseNumber}: ${asString(participant.licenseNumber)}`
: undefined,
]
.filter(Boolean)
.join("، ");
return {
label: roleLabels[role] ?? persianFieldPath(role),
value: value || PR.empty,

View File

@@ -67,7 +67,6 @@ export const PR = {
vehicleOwnerRole: "مالک وسیله نقلیه",
thirdPartyPolicyholderRole: "بیمه‌گذار شخص ثالث",
carBodyPolicyholderRole: "بیمه‌گذار بدنه",
unknown: "نامشخص",
data: "اطلاعات",
date: "تاریخ",
time: "زمان",

View File

@@ -10,7 +10,7 @@ import {
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
Length,
ValidateNested,
} from "class-validator";
@@ -51,14 +51,6 @@ export class InquiryParticipantInputDto {
@IsEnum(InquiryParticipantRole)
sameAs?: InquiryParticipantRole;
@ApiPropertyOptional({
description:
"Only for the damaged party's third-party policyholder when the identity is genuinely unknown.",
})
@IsOptional()
@IsBoolean()
unknown?: boolean;
@ApiPropertyOptional({ example: "0012345678" })
@IsOptional()
@IsString()
@@ -111,10 +103,23 @@ export class InquiryVehicleInputDto {
@Type(() => InquiryPlateDto)
previousPlate?: InquiryPlateDto;
@ApiPropertyOptional({ maxLength: 17, example: "NAAM01E15HK123456" })
@ApiPropertyOptional({
description:
"National code of the policyholder associated with previousPlate. Required only for RECENTLY_TRANSFERRED vehicles.",
example: "0012345678",
})
@IsOptional()
@IsString()
@MaxLength(17)
previousPolicyholderNationalCode?: string;
@ApiPropertyOptional({
minLength: 17,
maxLength: 17,
example: "NAAM01E15HK123456",
})
@IsOptional()
@IsString()
@Length(17, 17)
vin?: string;
@ApiPropertyOptional({

View File

@@ -92,6 +92,7 @@ import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/clai
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { sanitizeStoredInquiryParticipants } from "src/request-management/inquiry-participant-resolver";
import {
GetClaimListV2ResponseDto,
ClaimListItemV2Dto,
@@ -4551,7 +4552,12 @@ export class ExpertClaimService {
(doc as Record<string, unknown>).updatedAtFormatted =
`${updatedDate} ${updatedTime}`;
docRec.parties = enrichBlamePartiesForAgreementView(docRec);
docRec.parties = enrichBlamePartiesForAgreementView(docRec).map(
(party) => ({
...party,
participants: sanitizeStoredInquiryParticipants(party.participants),
}),
);
return docRec;
}
@@ -4577,7 +4583,11 @@ export class ExpertClaimService {
if (Array.isArray(parties)) {
out.parties = parties.map((p: any) =>
p && typeof p === "object"
? { ...p, vehicle: this.sanitizeVehicleInquiryForApi(p.vehicle) }
? {
...p,
participants: sanitizeStoredInquiryParticipants(p.participants),
vehicle: this.sanitizeVehicleInquiryForApi(p.vehicle),
}
: p,
);
}

View File

@@ -46,6 +46,7 @@ import { toJalaliDateAndTime } from "src/helpers/date-jalali";
import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agreement-decision";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { sanitizeStoredInquiryParticipants } from "src/request-management/inquiry-participant-resolver";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
@@ -449,6 +450,7 @@ export class ExpertInsurerService {
if (!party || typeof party !== "object") return party;
return {
...party,
participants: sanitizeStoredInquiryParticipants(party.participants),
person: this.mapPersonForInsurer(party.person, clientNames),
vehicle: this.sanitizeVehicleInquiry(party.vehicle),
};

View File

@@ -66,7 +66,6 @@ export class InquiryParticipant {
@Prop({ type: Boolean }) hasDrivingLicense?: boolean;
@Prop() licenseNumber?: string;
@Prop() licenseType?: string;
@Prop({ type: Boolean }) unknown?: boolean;
}
export const InquiryParticipantSchema =
SchemaFactory.createForClass(InquiryParticipant);
@@ -94,6 +93,7 @@ export class Vehicle {
@Prop({ enum: ["CURRENT", "RECENTLY_TRANSFERRED"] })
registrationState?: "CURRENT" | "RECENTLY_TRANSFERRED";
@Prop() previousPlateId?: string;
@Prop() previousPolicyholderNationalCode?: string;
/**
* Full external inquiry payload (Tejarat/SandHub) stored as-is

View File

@@ -161,6 +161,7 @@ export class FirstPartyDetail {
@Prop() registrationState?: string;
@Prop() previousPlateId?: string;
@Prop() previousPolicyholderNationalCode?: string;
@Prop() vehicleVin?: string;
}
@@ -209,6 +210,7 @@ export class SecondPartyDetail {
@Prop() registrationState?: string;
@Prop() previousPlateId?: string;
@Prop() previousPolicyholderNationalCode?: string;
@Prop() vehicleVin?: string;
}

View File

@@ -115,4 +115,34 @@ describe("inquiry participant persistence", () => {
expect(() => (query as any)._castUpdate(query.getUpdate())).not.toThrow();
});
it("stores the previous policyholder national code with transferred vehicle data", () => {
const BlameRequestModel = model<BlameRequest>(
"PreviousPolicyholderPersistenceTest",
BlameRequestSchema,
);
const request = new BlameRequestModel({
requestNo: "BL-PREVIOUS-POLICYHOLDER",
publicId: "PREVIOUS-POLICYHOLDER",
type: BlameRequestType.THIRD_PARTY,
parties: [
{
role: PartyRole.FIRST,
person: {},
vehicle: {
plateId: "22-44-ب-111",
previousPlateId: "33-55-ج-222",
previousPolicyholderNationalCode: "0098765432",
registrationState: "RECENTLY_TRANSFERRED",
vin: "NAAM01E15HK123456",
},
},
],
});
expect(request.validateSync()).toBeUndefined();
expect(request.parties[0].vehicle?.previousPolicyholderNationalCode).toBe(
"0098765432",
);
});
});

View File

@@ -14,6 +14,7 @@ import {
resolveInquirySubjects,
resolveInquiryVehicle,
runPlateInquiryWithFallback,
sanitizeStoredInquiryParticipants,
vehiclePlateCandidates,
} from "./inquiry-participant-resolver";
@@ -55,6 +56,27 @@ describe("inquiry participant resolver", () => {
).toThrow(BadRequestException);
});
it("requires the previous policyholder national code for a recent transfer", () => {
expect(() =>
resolveInquiryVehicle({
registrationState: VehicleRegistrationState.RECENTLY_TRANSFERRED,
currentPlate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
},
previousPlate: {
leftDigits: "55",
centerAlphabet: "ج",
centerDigits: "222",
ir: "33",
},
vin: "NAAM01E15HK123456",
}),
).toThrow("previousPolicyholderNationalCode");
});
it("defaults an omitted registration state to CURRENT", () => {
expect(
resolveInquiryVehicle({
@@ -68,6 +90,75 @@ describe("inquiry participant resolver", () => {
).toBe(VehicleRegistrationState.CURRENT);
});
it("requires an exact 17-character VIN whenever VIN is provided", () => {
expect(() =>
resolveInquiryVehicle({
registrationState: VehicleRegistrationState.CURRENT,
currentPlate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
},
vin: "TOO-SHORT",
}),
).toThrow("vehicle.vin must contain exactly 17 characters.");
});
it("rejects an incomplete current plate", () => {
expect(() =>
resolveInquiryVehicle({
currentPlate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "",
ir: "22",
},
}),
).toThrow("vehicle.currentPlate.centerDigits is required.");
});
it("rejects invalid vehicle choice values", () => {
expect(() =>
resolveInquiryVehicle({
registrationState: "SOLD" as any,
currentPlate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
},
}),
).toThrow("vehicle.registrationState must be CURRENT or RECENTLY_TRANSFERRED.");
expect(() =>
resolveInquiryVehicle({
currentPlate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
},
isNewCar: "false" as any,
}),
).toThrow("vehicle.isNewCar must be a boolean.");
});
it("rejects a previous policyholder national code for a current registration", () => {
expect(() =>
resolveInquiryVehicle({
registrationState: VehicleRegistrationState.CURRENT,
currentPlate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
},
previousPolicyholderNationalCode: "0098765432",
}),
).toThrow(BadRequestException);
});
it("rejects the removed flat inquiry contract", () => {
expect(() =>
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
@@ -238,6 +329,7 @@ describe("inquiry participant resolver", () => {
currentPlate,
previousPlate,
vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}),
).toEqual([
{ kind: "CURRENT", plate: currentPlate },
@@ -293,6 +385,7 @@ describe("inquiry participant resolver", () => {
currentPlate,
previousPlate,
vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
},
fallbackCurrentPlate: currentPlate,
query,
@@ -301,6 +394,8 @@ describe("inquiry participant resolver", () => {
});
expect(query).toHaveBeenCalledTimes(2);
expect(query).toHaveBeenNthCalledWith(1, currentPlate, "CURRENT");
expect(query).toHaveBeenNthCalledWith(2, previousPlate, "PREVIOUS");
expect(result.plateKind).toBe("PREVIOUS");
expect(result.attempts).toMatchObject([
{ plateKind: "CURRENT", succeeded: false, error: "not found" },
@@ -449,6 +544,7 @@ describe("inquiry participant resolver", () => {
currentPlate,
previousPlate,
vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}),
fallbackCurrentPlate: currentPlate,
query,
@@ -487,6 +583,7 @@ describe("inquiry participant resolver", () => {
currentPlate,
previousPlate,
vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}),
fallbackCurrentPlate: currentPlate,
query: async () => ({ mapped: { CompanyName: "پارسیان" } }),
@@ -526,6 +623,7 @@ describe("inquiry participant resolver", () => {
ir: "33",
},
vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}),
fallbackCurrentPlate: currentPlate,
query,
@@ -536,7 +634,7 @@ describe("inquiry participant resolver", () => {
expect(query).toHaveBeenCalledTimes(1);
});
it("allows an explicitly unknown policyholder only for a damaged-party submission", () => {
it("rejects the removed unknown policyholder option", () => {
const input = {
driver: {
nationalCode: "0012345678",
@@ -548,19 +646,24 @@ describe("inquiry participant resolver", () => {
};
expect(() =>
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, input),
).toThrow(BadRequestException);
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, input as any),
).toThrow("THIRD_PARTY_POLICYHOLDER does not support the unknown option.");
});
const resolved = resolveInquiryParticipants(
BlameRequestType.THIRD_PARTY,
input,
{ allowUnknownThirdPartyPolicyholder: true },
);
it("strips the removed unknown field from historical participant output", () => {
expect(
participantForRole(
resolved,
InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER,
),
).toMatchObject({ unknown: true });
sanitizeStoredInquiryParticipants([
{
participantId: "THIRD_PARTY_POLICYHOLDER",
nationalCode: "0012345678",
unknown: true,
},
]),
).toEqual([
{
participantId: "THIRD_PARTY_POLICYHOLDER",
nationalCode: "0012345678",
},
]);
});
});

View File

@@ -18,11 +18,6 @@ export interface ResolvedInquiryParticipant {
hasDrivingLicense?: boolean;
licenseNumber?: string;
licenseType?: string;
unknown?: boolean;
}
export interface InquiryParticipantResolutionOptions {
allowUnknownThirdPartyPolicyholder?: boolean;
}
export interface InquiryParticipantRoleAssignments {
@@ -96,6 +91,22 @@ export type ResolvedInquiryVehicle = InquiryVehicleInputDto & {
registrationState: VehicleRegistrationState;
};
function assertCompleteInquiryPlate(
plate: InquiryVehicleInputDto["currentPlate"],
path: "vehicle.currentPlate" | "vehicle.previousPlate",
): void {
for (const field of [
"leftDigits",
"centerAlphabet",
"centerDigits",
"ir",
] as const) {
if (plate?.[field] == null || String(plate[field]).trim() === "") {
throw new BadRequestException(`${path}.${field} is required.`);
}
}
}
export function participantForRole(
resolved: ResolvedInquiryParticipants,
role: InquiryParticipantRole,
@@ -127,6 +138,23 @@ export function participantForStoredPartyRole(
);
}
/** Remove fields retired from the participant contract from historical records. */
export function sanitizeStoredInquiryParticipants(
participants: unknown,
): Array<Record<string, any>> | undefined {
if (!Array.isArray(participants)) return undefined;
return participants.map((participant) => {
const plain =
participant &&
typeof participant === "object" &&
typeof (participant as { toObject?: unknown }).toObject === "function"
? (participant as { toObject: () => Record<string, any> }).toObject()
: { ...(participant as Record<string, any>) };
delete plain.unknown;
return plain;
});
}
const ROLE_FIELDS: Record<
InquiryParticipantRole,
keyof InquiryParticipantFieldsDto
@@ -185,7 +213,6 @@ function requiredIdentity(
export function resolveInquiryParticipants(
caseType: BlameRequestType,
input: Partial<InquiryParticipantFieldsDto> & Record<string, any>,
options: InquiryParticipantResolutionOptions = {},
): ResolvedInquiryParticipants {
assertStructuredInquiryInput(input);
const hasRoleCompleteInput = Object.values(ROLE_FIELDS).some(
@@ -232,32 +259,7 @@ export function resolveInquiryParticipants(
resolving.add(role);
let participantId: string;
if (
role === InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER &&
value.unknown === true
) {
if (!options.allowUnknownThirdPartyPolicyholder) {
throw new BadRequestException(
"An unknown third-party policyholder is allowed only for the damaged party.",
);
}
const hasIdentityFields = Object.entries(value).some(
([field, fieldValue]) => field !== "unknown" && fieldValue != null,
);
if (hasIdentityFields) {
throw new BadRequestException(
`${role} must contain either unknown=true or person fields, not both.`,
);
}
const participant: ResolvedInquiryParticipant = {
participantId: role,
nationalCode: "",
birthday: "",
unknown: true,
};
participants.set(participant.participantId, participant);
participantId = participant.participantId;
} else if (value.unknown != null) {
if (Object.prototype.hasOwnProperty.call(value, "unknown")) {
throw new BadRequestException(
`${role} does not support the unknown option.`,
);
@@ -314,28 +316,59 @@ export function resolveInquiryParticipants(
export function resolveInquiryVehicle(
input: InquiryVehicleInputDto,
): ResolvedInquiryVehicle {
if (!input) {
throw new BadRequestException("vehicle is required.");
}
const registrationState =
input.registrationState ?? VehicleRegistrationState.CURRENT;
if (!Object.values(VehicleRegistrationState).includes(registrationState)) {
throw new BadRequestException(
"vehicle.registrationState must be CURRENT or RECENTLY_TRANSFERRED.",
);
}
if (input.isNewCar != null && typeof input.isNewCar !== "boolean") {
throw new BadRequestException("vehicle.isNewCar must be a boolean.");
}
const previousPolicyholderNationalCode = String(
input.previousPolicyholderNationalCode ?? "",
).trim();
if (!input.currentPlate) {
throw new BadRequestException("vehicle.currentPlate is required.");
}
assertCompleteInquiryPlate(input.currentPlate, "vehicle.currentPlate");
const vin = String(input.vin ?? "").trim();
if (vin && vin.length !== 17) {
throw new BadRequestException(
"vehicle.vin must contain exactly 17 characters.",
);
}
if (
registrationState === VehicleRegistrationState.RECENTLY_TRANSFERRED &&
(!input.previousPlate || !String(input.vin ?? "").trim())
(!input.previousPlate || !vin || !previousPolicyholderNationalCode)
) {
throw new BadRequestException(
"RECENTLY_TRANSFERRED requires previousPlate and vin.",
"RECENTLY_TRANSFERRED requires previousPlate, vin, and previousPolicyholderNationalCode.",
);
}
if (
registrationState !== VehicleRegistrationState.RECENTLY_TRANSFERRED &&
input.previousPlate
(input.previousPlate || input.previousPolicyholderNationalCode != null)
) {
throw new BadRequestException(
"previousPlate is only allowed for RECENTLY_TRANSFERRED vehicles.",
"previousPlate and previousPolicyholderNationalCode are only allowed for RECENTLY_TRANSFERRED vehicles.",
);
}
return { ...input, registrationState };
if (input.previousPlate) {
assertCompleteInquiryPlate(input.previousPlate, "vehicle.previousPlate");
}
return {
...input,
registrationState,
...(vin ? { vin } : {}),
...(previousPolicyholderNationalCode
? { previousPolicyholderNationalCode }
: {}),
};
}
export function vehiclePlateCandidates(input?: ResolvedInquiryVehicle): Array<{
@@ -459,7 +492,10 @@ export function isPolicyNotFoundError(error: unknown): boolean {
export async function runPlateInquiryWithFallback<T>(options: {
vehicle?: ResolvedInquiryVehicle;
fallbackCurrentPlate: InquiryVehicleInputDto["currentPlate"];
query: (plate: InquiryVehicleInputDto["currentPlate"]) => Promise<T>;
query: (
plate: InquiryVehicleInputDto["currentPlate"],
plateKind: "CURRENT" | "PREVIOUS",
) => Promise<T>;
isUsable: (value: T) => boolean;
mappedValue: (value: T) => Record<string, any>;
shouldFallbackOnError?: (error: unknown) => boolean;
@@ -492,7 +528,7 @@ export async function runPlateInquiryWithFallback<T>(options: {
const candidate = candidates[index];
const isLast = index === candidates.length - 1;
try {
const value = await options.query(candidate.plate);
const value = await options.query(candidate.plate, candidate.kind);
const usable = options.isUsable(value);
if (!usable) {
attempts.push({
@@ -562,13 +598,8 @@ export async function runPlateInquiryWithFallback<T>(options: {
export function normalizeInquirySubmission<T extends Record<string, any>>(
caseType: BlameRequestType,
input: T,
options: InquiryParticipantResolutionOptions = {},
): NormalizedInquirySubmission<T> {
const participants = resolveInquiryParticipants(
caseType,
input as any,
options,
);
const participants = resolveInquiryParticipants(caseType, input as any);
const driver = participantForRole(
participants,
InquiryParticipantRole.DRIVER,

View File

@@ -0,0 +1,44 @@
import { RequestManagementService } from "./request-management.service";
describe("damaged-party inquiry requirements", () => {
const getService = () => {
const service = Object.create(
RequestManagementService.prototype,
) as RequestManagementService;
(service as any).sandHubService = {
getShebaValidation: jest.fn().mockResolvedValue({ valid: true }),
};
return service;
};
it("requires Sheba when the claimant/damaged-party validation runs", async () => {
const service = getService();
await expect(
(service as any).validateShebaV3(
undefined,
"0012345678",
"client-id",
),
).rejects.toThrow("sheba is required for the damaged party.");
expect(
(service as any).sandHubService.getShebaValidation,
).not.toHaveBeenCalled();
});
it("validates damaged-party Sheba against the vehicle owner's national code", async () => {
const service = getService();
await (service as any).validateShebaV3(
"IR123456789012345678901234",
"0012345678",
"client-id",
);
expect(
(service as any).sandHubService.getShebaValidation,
).toHaveBeenCalledWith("0012345678", "IR123456789012345678901234", {
clientId: "client-id",
});
});
});

View File

@@ -0,0 +1,130 @@
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { RequestManagementService } from "./request-management.service";
describe("RequestManagementService previous policyholder routing", () => {
const currentPlate = {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
};
const previousPlate = {
leftDigits: "55",
centerAlphabet: "ج",
centerDigits: "222",
ir: "33",
};
const vehicle = {
registrationState: "RECENTLY_TRANSFERRED",
currentPlate,
previousPlate,
previousPolicyholderNationalCode: "0098765432",
vin: "NAAM01E15HK123456",
};
function participantInput(caseType: BlameRequestType) {
return {
driver: {
nationalCode: "0011111111",
birthday: "1370/01/01",
hasDrivingLicense: false,
},
vehicleOwner: { sameAs: "DRIVER" },
thirdPartyPolicyholder: {
nationalCode: "0022222222",
birthday: "1360/02/02",
},
...(caseType === BlameRequestType.CAR_BODY
? {
carBodyPolicyholder: {
nationalCode: "0033333333",
birthday: "1350/03/03",
},
}
: {}),
vehicle,
};
}
it("uses the previous code only for the previous-plate third-party lookup", async () => {
const service = Object.create(RequestManagementService.prototype) as any;
service.sandHubService = {
getTejaratBlockInquiry: jest
.fn()
.mockResolvedValueOnce({ raw: {}, mapped: {} })
.mockResolvedValueOnce({
raw: {},
mapped: {
CompanyName: "پارسیان",
VinNumberField: vehicle.vin,
},
}),
};
const submission = service.normalizeInquiryInput(
BlameRequestType.THIRD_PARTY,
participantInput(BlameRequestType.THIRD_PARTY),
);
const result = await service.getThirdPartyPlateInquiry(submission);
expect(result.plateKind).toBe("PREVIOUS");
expect(
service.sandHubService.getTejaratBlockInquiry,
).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
plate: currentPlate,
nationalCodeOfInsurer: "0022222222",
}),
undefined,
);
expect(
service.sandHubService.getTejaratBlockInquiry,
).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
plate: previousPlate,
nationalCodeOfInsurer: "0098765432",
}),
undefined,
);
});
it("uses the previous code only for the previous-plate car-body lookup", async () => {
const service = Object.create(RequestManagementService.prototype) as any;
service.sandHubService = {
getCarBodyInquiry: jest
.fn()
.mockResolvedValueOnce({ raw: {}, mapped: {} })
.mockResolvedValueOnce({
raw: {},
mapped: {
policyNumber: "BODY-1",
VinNumberField: vehicle.vin,
},
}),
};
const submission = service.normalizeInquiryInput(
BlameRequestType.CAR_BODY,
participantInput(BlameRequestType.CAR_BODY),
);
const result = await service.getCarBodyPlateInquiry(submission);
expect(result.plateKind).toBe("PREVIOUS");
expect(service.sandHubService.getCarBodyInquiry).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
plate: currentPlate,
nationalCodeOfInsurer: "0033333333",
}),
);
expect(service.sandHubService.getCarBodyInquiry).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
plate: previousPlate,
nationalCodeOfInsurer: "0098765432",
}),
);
});
});

View File

@@ -130,6 +130,7 @@ import {
normalizeInquirySubmission,
resolveInquirySubjects,
runPlateInquiryWithFallback,
sanitizeStoredInquiryParticipants,
} from "./inquiry-participant-resolver";
/**
@@ -311,12 +312,8 @@ export class RequestManagementService {
private normalizeInquiryInput<T extends Record<string, any>>(
type: BlameRequestType,
input: T,
partyRole?: PartyRole,
): NormalizedInquirySubmission<T> {
return normalizeInquirySubmission(type, input, {
allowUnknownThirdPartyPolicyholder:
type === BlameRequestType.THIRD_PARTY && partyRole === PartyRole.SECOND,
});
return normalizeInquirySubmission(type, input);
}
private applyInquiryParticipantsToParty(
@@ -334,6 +331,8 @@ export class RequestManagementService {
party.vehicle.previousPlateId = submission.vehicle.previousPlate
? this.plateToPlateIdString(submission.vehicle.previousPlate)
: undefined;
party.vehicle.previousPolicyholderNationalCode =
submission.vehicle.previousPolicyholderNationalCode;
if (submission.vehicle.vin) party.vehicle.vin = submission.vehicle.vin;
}
}
@@ -354,24 +353,18 @@ 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 subjects = resolveInquirySubjects(submission);
const result = await runPlateInquiryWithFallback({
vehicle: submission.vehicle,
fallbackCurrentPlate: submission.dto.plate,
query: (plate) =>
query: (plate, plateKind) =>
this.sandHubService.getTejaratBlockInquiry(
{
plate: plate as any,
nationalCodeOfInsurer: subjects.thirdPartyPolicyNationalCode,
nationalCodeOfInsurer:
plateKind === "PREVIOUS"
? submission.vehicle!.previousPolicyholderNationalCode!
: subjects.thirdPartyPolicyNationalCode,
},
options,
),
@@ -401,9 +394,12 @@ export class RequestManagementService {
const result = await runPlateInquiryWithFallback({
vehicle: submission.vehicle,
fallbackCurrentPlate: submission.dto.plate,
query: (plate) =>
query: (plate, plateKind) =>
this.sandHubService.getCarBodyInquiry({
nationalCodeOfInsurer: policyholderNationalCode,
nationalCodeOfInsurer:
plateKind === "PREVIOUS"
? submission.vehicle!.previousPolicyholderNationalCode!
: policyholderNationalCode,
plate: plate as any,
}),
isUsable: (value) =>
@@ -435,9 +431,6 @@ export class RequestManagementService {
"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(
{
@@ -446,7 +439,7 @@ export class RequestManagementService {
},
options,
);
return { ...result, skipped: false };
return result;
}
private async runParticipantPersonalInquiries(
@@ -457,9 +450,7 @@ export class RequestManagementService {
): Promise<void> {
const participants = submission.participants.legacy
? [submission.thirdPartyPolicyholder]
: submission.participants.participants.filter(
(participant) => !participant.unknown,
);
: submission.participants.participants;
const results: Record<string, unknown> = {};
for (const participant of participants) {
@@ -598,7 +589,6 @@ export class RequestManagementService {
const personal: Record<string, unknown> = {};
for (const participant of submission.participants.participants) {
if (participant.unknown) continue;
personal[participant.participantId] =
await this.sandHubService.getPersonalInquiry(
participant.nationalCode,
@@ -664,6 +654,8 @@ export class RequestManagementService {
[`${prefix}.previousPlateId`]: submission.vehicle?.previousPlate
? this.plateToPlateIdString(submission.vehicle.previousPlate)
: undefined,
[`${prefix}.previousPolicyholderNationalCode`]:
submission.vehicle?.previousPolicyholderNationalCode,
[`${prefix}.vehicleVin`]: submission.vehicle?.vin,
[`${prefix}.policyInquiry`]: audit,
},
@@ -1732,12 +1724,9 @@ export class RequestManagementService {
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) {
@@ -1801,19 +1790,13 @@ export class RequestManagementService {
req,
"thirdParty",
role,
!inquiry.skipped,
true,
{
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) {
@@ -1882,7 +1865,7 @@ export class RequestManagementService {
// Find client by company code
const clientName = inquiryMapped?.CompanyName;
if (!clientName && !policyholderUnknown) {
if (!clientName) {
const error = new BadRequestException(
`CompanyName missing from inquiry response`,
);
@@ -2183,13 +2166,10 @@ export class RequestManagementService {
const inquirySubmission = this.normalizeInquiryInput(
req.type,
body,
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) {
@@ -2246,17 +2226,11 @@ export class RequestManagementService {
req,
"thirdParty",
role,
!inquiry.skipped,
true,
{
source: "ESG_VIN_INQUIRY",
raw: inquiryRaw,
mapped: inquiryMapped,
...(inquiry.skipped
? {
skipped: true,
reason: "Third-party policyholder is unknown",
}
: {}),
},
);
} catch (err: any) {
@@ -2319,7 +2293,7 @@ export class RequestManagementService {
// Resolve insurer client from inquiry response
const clientName = inquiryMapped?.CompanyName;
if (!clientName && !policyholderUnknown) {
if (!clientName) {
const error = new BadRequestException(
"CompanyName missing from VIN inquiry response",
);
@@ -3482,8 +3456,6 @@ export class RequestManagementService {
const clientName = sandHubReport?.CompanyName;
const companyCode = sandHubReport?.CompanyCode;
const policyholderUnknown =
inquirySubmission?.thirdPartyPolicyholder.unknown === true;
const client = clientName
? await this.clientService.findOrCreateClientByCompanyCode(
companyCode,
@@ -3491,7 +3463,7 @@ export class RequestManagementService {
)
: null;
if (!client && !policyholderUnknown) {
if (!client) {
throw new HttpException("Client not found", HttpStatus.CONFLICT);
}
@@ -3529,13 +3501,11 @@ export class RequestManagementService {
plate: body.plate,
nationalCode: body.nationalCodeOfInsurer,
};
const sandHubDoc = policyInquiry?.skipped
? null
: await this.sandHubService.sandHubDocumentCreator(
user.sub,
request["_doc"]._id,
sandHubDocData,
);
const sandHubDoc = await this.sandHubService.sandHubDocumentCreator(
user.sub,
request["_doc"]._id,
sandHubDocData,
);
if (partyType === "firstParty" && sandHubDoc) {
await this.requestManagementDbService.findAndUpdate(
@@ -3592,6 +3562,8 @@ export class RequestManagementService {
?.previousPlate
? this.plateToPlateIdString(inquirySubmission.vehicle.previousPlate)
: undefined;
setFields[`${partyDetails}.previousPolicyholderNationalCode`] =
inquirySubmission.vehicle?.previousPolicyholderNationalCode;
setFields[`${partyDetails}.vehicleVin`] =
inquirySubmission.vehicle?.vin;
}
@@ -3704,7 +3676,6 @@ export class RequestManagementService {
const inquirySubmission = this.normalizeInquiryInput(
request.type as BlameRequestType,
body,
partyRole,
);
body = inquirySubmission.dto as unknown as AddPlateDto;
@@ -6641,7 +6612,6 @@ export class RequestManagementService {
participantId: participant.participantId,
fullName: participant.fullName,
hasDrivingLicense: participant.hasDrivingLicense,
unknown: participant.unknown,
}))
: undefined;
const evidenceRaw = party.evidence as Record<string, unknown> | undefined;
@@ -6951,7 +6921,6 @@ export class RequestManagementService {
const inquirySubmission = this.normalizeInquiryInput(
req.type,
formData.firstPartyPlate,
PartyRole.FIRST,
);
this.recordInquiryParticipantContext(
req,
@@ -7139,6 +7108,8 @@ export class RequestManagementService {
previousPlateId: inquirySubmission.vehicle?.previousPlate
? this.plateToPlateIdString(inquirySubmission.vehicle.previousPlate)
: undefined,
previousPolicyholderNationalCode:
inquirySubmission.vehicle?.previousPolicyholderNationalCode,
vin: inquirySubmission.vehicle?.vin,
inquiry: {
...sandHubReport,
@@ -7279,7 +7250,6 @@ export class RequestManagementService {
const inquirySubmission = this.normalizeInquiryInput(
req.type,
plateDto,
role,
);
this.recordInquiryParticipantContext(req, role, inquirySubmission);
const existingPartyIndex = this.getPartyIndex(req, role);
@@ -7290,8 +7260,6 @@ export class RequestManagementService {
);
}
plateDto = inquirySubmission.dto;
const policyholderUnknown =
inquirySubmission.thirdPartyPolicyholder.unknown === true;
let inquiry: any;
try {
if (inquirySubmission.participants.legacy) {
@@ -7332,19 +7300,13 @@ export class RequestManagementService {
req,
"thirdParty",
role,
!inquiry.skipped,
true,
{
source: inquiry.offline?.source ?? "TEJARAT_BLOCK_INQUIRY",
plateKind: inquiry.plateKind,
attempts: inquiry.attempts,
raw: inquiry.raw,
mapped: inquiry.mapped,
...(inquiry.skipped
? {
skipped: true,
reason: "Third-party policyholder is unknown",
}
: {}),
},
);
const clientName =
@@ -7358,7 +7320,7 @@ export class RequestManagementService {
)
: await this.clientService.findOne({ clientName })
: null;
if (!client && !policyholderUnknown) {
if (!client) {
const error = new NotFoundException(
`Client not found for company: ${clientName}`,
);
@@ -7434,6 +7396,8 @@ export class RequestManagementService {
previousPlateId: inquirySubmission.vehicle?.previousPlate
? this.plateToPlateIdString(inquirySubmission.vehicle.previousPlate)
: undefined,
previousPolicyholderNationalCode:
inquirySubmission.vehicle?.previousPolicyholderNationalCode,
vin: inquirySubmission.vehicle?.vin,
inquiry: sandHubReport,
},
@@ -8071,7 +8035,6 @@ export class RequestManagementService {
const firstInquirySubmission = this.normalizeInquiryInput(
BlameRequestType.THIRD_PARTY,
formData.firstPartyPlate,
PartyRole.FIRST,
);
const firstPartyPlate = firstInquirySubmission.dto;
let firstPolicyInquiry: any;
@@ -8179,6 +8142,8 @@ export class RequestManagementService {
firstInquirySubmission.vehicle.previousPlate,
)
: undefined;
firstPartyDetails.previousPolicyholderNationalCode =
firstInquirySubmission.vehicle?.previousPolicyholderNationalCode;
firstPartyDetails.vehicleVin = firstInquirySubmission.vehicle?.vin;
}
@@ -8209,7 +8174,6 @@ export class RequestManagementService {
const secondInquirySubmission = this.normalizeInquiryInput(
BlameRequestType.THIRD_PARTY,
formData.secondParty.plate,
PartyRole.SECOND,
);
let secondPolicyInquiry: any;
try {
@@ -8243,8 +8207,6 @@ export class RequestManagementService {
const companyCode = sandHubReport?.CompanyCode;
// Try to find client by company code first (more reliable)
const policyholderUnknown =
secondInquirySubmission.thirdPartyPolicyholder.unknown === true;
const client = clientName
? companyCode
? await this.clientService.findOrCreateClientByCompanyCode(
@@ -8254,7 +8216,7 @@ export class RequestManagementService {
: await this.clientService.findOne({ clientName: clientName })
: null;
if (!client && !policyholderUnknown) {
if (!client) {
const error = new NotFoundException(
`Client not found for company: ${clientName}${companyCode ? ` (code: ${companyCode})` : ""}`,
);
@@ -8319,6 +8281,8 @@ export class RequestManagementService {
secondInquirySubmission.vehicle.previousPlate,
)
: undefined;
secondPartyDetails.previousPolicyholderNationalCode =
secondInquirySubmission.vehicle?.previousPolicyholderNationalCode;
secondPartyDetails.vehicleVin = secondInquirySubmission.vehicle?.vin;
}
@@ -8554,7 +8518,6 @@ export class RequestManagementService {
const firstInquirySubmission = this.normalizeInquiryInput(
BlameRequestType.CAR_BODY,
formData.firstPartyPlate,
PartyRole.FIRST,
);
const firstPartyPlate = firstInquirySubmission.dto;
let thirdPartyPolicyInquiry: any;
@@ -8687,6 +8650,8 @@ export class RequestManagementService {
firstInquirySubmission.vehicle.previousPlate,
)
: undefined;
firstPartyDetails.previousPolicyholderNationalCode =
firstInquirySubmission.vehicle?.previousPolicyholderNationalCode;
firstPartyDetails.vehicleVin = firstInquirySubmission.vehicle?.vin;
}
firstPartyDetails.firstPartyClient.clientId =
@@ -9805,13 +9770,10 @@ export class RequestManagementService {
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;
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
let clientId: string | undefined;
const inquiryOptions = (cid?: string) =>
@@ -9838,19 +9800,13 @@ export class RequestManagementService {
req,
"thirdParty",
partyRole,
!inquiry.skipped,
true,
{
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) {
@@ -9900,7 +9856,7 @@ export class RequestManagementService {
const clientName = inquiryMapped?.CompanyName;
const companyCode = inquiryMapped?.CompanyCode;
if (!clientName && !policyholderUnknown) {
if (!clientName) {
const error = new BadRequestException(
`CompanyName missing from ${roleLabel} party inquiry response`,
);
@@ -10162,10 +10118,14 @@ export class RequestManagementService {
vehicleOwnerNationalCode: string,
clientId?: string,
): Promise<void> {
if (!sheba) return;
if (!String(sheba ?? "").trim()) {
throw new BadRequestException(
"sheba is required for the damaged party.",
);
}
await this.sandHubService.getShebaValidation(
vehicleOwnerNationalCode,
sheba,
sheba!.trim(),
clientId ? { clientId } : undefined,
);
}
@@ -10738,13 +10698,10 @@ export class RequestManagementService {
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) =>
@@ -10763,17 +10720,11 @@ export class RequestManagementService {
req,
"thirdParty",
partyRole,
!inquiry.skipped,
true,
{
source: "ESG_VIN_INQUIRY",
raw: inquiryRaw,
mapped: inquiryMapped,
...(inquiry.skipped
? {
skipped: true,
reason: "Third-party policyholder is unknown",
}
: {}),
},
);
} catch (err: any) {
@@ -10817,7 +10768,7 @@ export class RequestManagementService {
const clientName = inquiryMapped?.CompanyName;
const companyCode = inquiryMapped?.CompanyCode;
if (!clientName && !policyholderUnknown) {
if (!clientName) {
const error = new BadRequestException(
`CompanyName missing from ${roleLabel} party VIN inquiry response`,
);
@@ -12320,12 +12271,16 @@ export class RequestManagementService {
message:
"استعلام طرف مقصر با موفقیت انجام شد. اکنون می‌توانید لینک تقصیر را برای کاربر ارسال کنید.",
guiltyParty: {
participants: firstPartyAfter?.participants,
participants: sanitizeStoredInquiryParticipants(
firstPartyAfter?.participants,
),
participantRoles: firstPartyAfter?.participantRoles,
vehicle: firstPartyAfter?.vehicle
? {
plateId: firstPartyAfter.vehicle.plateId,
previousPlateId: firstPartyAfter.vehicle.previousPlateId,
previousPolicyholderNationalCode:
firstPartyAfter.vehicle.previousPolicyholderNationalCode,
registrationState: firstPartyAfter.vehicle.registrationState,
vin: firstPartyAfter.vehicle.vin,
name: firstPartyAfter.vehicle.name,
@@ -12435,13 +12390,17 @@ export class RequestManagementService {
message:
"استعلام VIN طرف مقصر با موفقیت انجام شد. اکنون می‌توانید لینک تقصیر را برای کاربر ارسال کنید.",
guiltyParty: {
participants: firstPartyAfter?.participants,
participants: sanitizeStoredInquiryParticipants(
firstPartyAfter?.participants,
),
participantRoles: firstPartyAfter?.participantRoles,
vehicle: firstPartyAfter?.vehicle
? {
vin: firstPartyAfter.vehicle.vin,
plateId: firstPartyAfter.vehicle.plateId,
previousPlateId: firstPartyAfter.vehicle.previousPlateId,
previousPolicyholderNationalCode:
firstPartyAfter.vehicle.previousPolicyholderNationalCode,
registrationState: firstPartyAfter.vehicle.registrationState,
name: firstPartyAfter.vehicle.name,
type: firstPartyAfter.vehicle.type,
@@ -12684,7 +12643,7 @@ export class RequestManagementService {
skipInitialFormStep: (req as any).skipInitialFormStep,
parties: ((req as any).parties ?? []).map((p: any) => ({
role: p.role,
participants: p.participants,
participants: sanitizeStoredInquiryParticipants(p.participants),
participantRoles: p.participantRoles,
person: {
phoneNumber: p.person?.phoneNumber,
@@ -12709,6 +12668,8 @@ export class RequestManagementService {
? {
plateId: p.vehicle.plateId,
previousPlateId: p.vehicle.previousPlateId,
previousPolicyholderNationalCode:
p.vehicle.previousPolicyholderNationalCode,
registrationState: p.vehicle.registrationState,
vin: p.vehicle.vin,
name: p.vehicle.name,
@@ -12844,7 +12805,7 @@ export class RequestManagementService {
requiresFileMakerApproval: plain.requiresFileMakerApproval,
parties: (plain.parties ?? []).map((p: any) => ({
role: p.role,
participants: p.participants,
participants: sanitizeStoredInquiryParticipants(p.participants),
participantRoles: p.participantRoles,
person: {
fullName: p.person?.fullName,
@@ -12873,6 +12834,8 @@ export class RequestManagementService {
? {
plateId: p.vehicle.plateId,
previousPlateId: p.vehicle.previousPlateId,
previousPolicyholderNationalCode:
p.vehicle.previousPolicyholderNationalCode,
registrationState: p.vehicle.registrationState,
vin: p.vehicle.vin,
name: p.vehicle.name,
@@ -13027,7 +12990,7 @@ export class RequestManagementService {
: null,
parties: (plain.parties ?? []).map((p: any) => ({
role: p.role,
participants: p.participants,
participants: sanitizeStoredInquiryParticipants(p.participants),
participantRoles: p.participantRoles,
person: {
fullName: p.person?.fullName,
@@ -13056,6 +13019,8 @@ export class RequestManagementService {
? {
plateId: p.vehicle.plateId,
previousPlateId: p.vehicle.previousPlateId,
previousPolicyholderNationalCode:
p.vehicle.previousPolicyholderNationalCode,
registrationState: p.vehicle.registrationState,
vin: p.vehicle.vin,
name: p.vehicle.name,