Compare commits

...

8 Commits

18 changed files with 536 additions and 209 deletions

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import {
IsNotEmpty, IsNotEmpty,
IsOptional, IsOptional,
IsString, IsString,
MaxLength, Length,
ValidateNested, ValidateNested,
} from "class-validator"; } from "class-validator";
@@ -51,14 +51,6 @@ export class InquiryParticipantInputDto {
@IsEnum(InquiryParticipantRole) @IsEnum(InquiryParticipantRole)
sameAs?: 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" }) @ApiPropertyOptional({ example: "0012345678" })
@IsOptional() @IsOptional()
@IsString() @IsString()
@@ -111,10 +103,23 @@ export class InquiryVehicleInputDto {
@Type(() => InquiryPlateDto) @Type(() => InquiryPlateDto)
previousPlate?: 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() @IsOptional()
@IsString() @IsString()
@MaxLength(17) previousPolicyholderNationalCode?: string;
@ApiPropertyOptional({
minLength: 17,
maxLength: 17,
example: "NAAM01E15HK123456",
})
@IsOptional()
@IsString()
@Length(17, 17)
vin?: string; vin?: string;
@ApiPropertyOptional({ @ApiPropertyOptional({

View File

@@ -1,11 +1,11 @@
/** /**
* Per-line and total caps for repair money. All values are **Toman** (no unit conversion in the API). * Per-line and total caps for repair money. All values are **Toman** (no unit conversion in the API).
*/ */
export const REPAIR_LINE_AMOUNT_TOMAN = { export const REPAIR_LINE_AMOUNT_TOMAN = { // IT IS RIAL FROM NOW ON
/** Below this is not credible for a priced repair line (e.g. 1,000 Toman). */ /** Below this is not credible for a priced repair line (e.g. 1,000 Toman). */
MIN: 10_000, MIN: 100_000,
/** Aligns with the total assessment cap; rejects absurd values (e.g. 100bn). */ /** Aligns with the total assessment cap; rejects absurd values (e.g. 100bn). */
MAX: 53_000_000, MAX: 530_000_000,
} as const; } as const;
/** Max sum of all priced + factor lines in one expert reply / validation (Toman). */ /** Max sum of all priced + factor lines in one expert reply / validation (Toman). */

View File

@@ -58,7 +58,7 @@ export class PartPricingV2Dto {
@ApiPropertyOptional({ @ApiPropertyOptional({
example: "5000000", example: "5000000",
description: description:
"Required for change lines; omitted for repair lines. Every supplied amount must be between 100,000 and 10,000,000,000 Toman.", "Required for change lines; omitted for repair lines. Every supplied amount must be between 1,000,000 and 100,000,000,000 Rial.",
}) })
@ValidateIf( @ValidateIf(
(part: PartPricingV2Dto) => (part: PartPricingV2Dto) =>
@@ -74,7 +74,7 @@ export class PartPricingV2Dto {
@ApiProperty({ @ApiProperty({
example: "2000000", example: "2000000",
description: description:
"Labor in Toman (integer string; 100,000 to 10,000,000,000).", "Labor in Rial (integer string; 1,000,000 to 100,000,000,000).",
}) })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@@ -84,7 +84,7 @@ export class PartPricingV2Dto {
@ApiProperty({ @ApiProperty({
example: "7000000", example: "7000000",
description: description:
"Line total in Toman (integer string; 100,000 to 10,000,000,000).", "Line total in Rial (integer string; 1,000,000 to 100,000,000,000).",
}) })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()

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

View File

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

View File

@@ -11,8 +11,8 @@ type ExpertReplyPricingPart = {
daghi?: { option?: unknown; price?: unknown }; daghi?: { option?: unknown; price?: unknown };
}; };
export const EXPERT_REPLY_MIN_AMOUNT_TOMAN = 100_000; export const EXPERT_REPLY_MIN_AMOUNT_TOMAN = 1_000_000; // It is Rial from now on but we managed to keep the variables named as *Toman so that we won't have conflicts elsewhere.
export const EXPERT_REPLY_MAX_AMOUNT_TOMAN = 10_000_000_000; export const EXPERT_REPLY_MAX_AMOUNT_TOMAN = 100_000_000_000; // It it Rial from now on
export type ExpertReplyPricingValidationError = { export type ExpertReplyPricingValidationError = {
message: string; message: string;
@@ -150,7 +150,7 @@ export function getExpertReplyPricingValidationError(
? "دستمزد" ? "دستمزد"
: "مبلغ کل"; : "مبلغ کل";
return localizedError( return localizedError(
`${fieldName} قطعه ${String(part.partId)} باید به‌صورت مبلغ صحیح و غیرمنفی (تومان) وارد شود.`, `${fieldName} قطعه ${String(part.partId)} باید به‌صورت مبلغ صحیح و غیرمنفی (ریال) وارد شود.`,
`parts[${index}].${field}`, `parts[${index}].${field}`,
"invalid_amount", "invalid_amount",
String(part.partId), String(part.partId),

View File

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

View File

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

View File

@@ -115,4 +115,34 @@ describe("inquiry participant persistence", () => {
expect(() => (query as any)._castUpdate(query.getUpdate())).not.toThrow(); 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, resolveInquirySubjects,
resolveInquiryVehicle, resolveInquiryVehicle,
runPlateInquiryWithFallback, runPlateInquiryWithFallback,
sanitizeStoredInquiryParticipants,
vehiclePlateCandidates, vehiclePlateCandidates,
} from "./inquiry-participant-resolver"; } from "./inquiry-participant-resolver";
@@ -55,6 +56,27 @@ describe("inquiry participant resolver", () => {
).toThrow(BadRequestException); ).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", () => { it("defaults an omitted registration state to CURRENT", () => {
expect( expect(
resolveInquiryVehicle({ resolveInquiryVehicle({
@@ -68,6 +90,75 @@ describe("inquiry participant resolver", () => {
).toBe(VehicleRegistrationState.CURRENT); ).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", () => { it("rejects the removed flat inquiry contract", () => {
expect(() => expect(() =>
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, { resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
@@ -238,6 +329,7 @@ describe("inquiry participant resolver", () => {
currentPlate, currentPlate,
previousPlate, previousPlate,
vin: "NAAM01E15HK123456", vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}), }),
).toEqual([ ).toEqual([
{ kind: "CURRENT", plate: currentPlate }, { kind: "CURRENT", plate: currentPlate },
@@ -293,6 +385,7 @@ describe("inquiry participant resolver", () => {
currentPlate, currentPlate,
previousPlate, previousPlate,
vin: "NAAM01E15HK123456", vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}, },
fallbackCurrentPlate: currentPlate, fallbackCurrentPlate: currentPlate,
query, query,
@@ -301,6 +394,8 @@ describe("inquiry participant resolver", () => {
}); });
expect(query).toHaveBeenCalledTimes(2); expect(query).toHaveBeenCalledTimes(2);
expect(query).toHaveBeenNthCalledWith(1, currentPlate, "CURRENT");
expect(query).toHaveBeenNthCalledWith(2, previousPlate, "PREVIOUS");
expect(result.plateKind).toBe("PREVIOUS"); expect(result.plateKind).toBe("PREVIOUS");
expect(result.attempts).toMatchObject([ expect(result.attempts).toMatchObject([
{ plateKind: "CURRENT", succeeded: false, error: "not found" }, { plateKind: "CURRENT", succeeded: false, error: "not found" },
@@ -449,6 +544,7 @@ describe("inquiry participant resolver", () => {
currentPlate, currentPlate,
previousPlate, previousPlate,
vin: "NAAM01E15HK123456", vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}), }),
fallbackCurrentPlate: currentPlate, fallbackCurrentPlate: currentPlate,
query, query,
@@ -487,6 +583,7 @@ describe("inquiry participant resolver", () => {
currentPlate, currentPlate,
previousPlate, previousPlate,
vin: "NAAM01E15HK123456", vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}), }),
fallbackCurrentPlate: currentPlate, fallbackCurrentPlate: currentPlate,
query: async () => ({ mapped: { CompanyName: "پارسیان" } }), query: async () => ({ mapped: { CompanyName: "پارسیان" } }),
@@ -526,6 +623,7 @@ describe("inquiry participant resolver", () => {
ir: "33", ir: "33",
}, },
vin: "NAAM01E15HK123456", vin: "NAAM01E15HK123456",
previousPolicyholderNationalCode: "0098765432",
}), }),
fallbackCurrentPlate: currentPlate, fallbackCurrentPlate: currentPlate,
query, query,
@@ -536,7 +634,7 @@ describe("inquiry participant resolver", () => {
expect(query).toHaveBeenCalledTimes(1); 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 = { const input = {
driver: { driver: {
nationalCode: "0012345678", nationalCode: "0012345678",
@@ -548,19 +646,24 @@ describe("inquiry participant resolver", () => {
}; };
expect(() => expect(() =>
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, input), resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, input as any),
).toThrow(BadRequestException); ).toThrow("THIRD_PARTY_POLICYHOLDER does not support the unknown option.");
});
const resolved = resolveInquiryParticipants( it("strips the removed unknown field from historical participant output", () => {
BlameRequestType.THIRD_PARTY,
input,
{ allowUnknownThirdPartyPolicyholder: true },
);
expect( expect(
participantForRole( sanitizeStoredInquiryParticipants([
resolved, {
InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER, participantId: "THIRD_PARTY_POLICYHOLDER",
), nationalCode: "0012345678",
).toMatchObject({ unknown: true }); unknown: true,
},
]),
).toEqual([
{
participantId: "THIRD_PARTY_POLICYHOLDER",
nationalCode: "0012345678",
},
]);
}); });
}); });

View File

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

View File

@@ -5,6 +5,7 @@ import {
} from "@nestjs/common"; } from "@nestjs/common";
import { ExternalInquirySettingsService } from "src/client/external-inquiry-settings.service"; import { ExternalInquirySettingsService } from "src/client/external-inquiry-settings.service";
import { SandHubDetailDto } from "./dto/sand-hub.dto"; import { SandHubDetailDto } from "./dto/sand-hub.dto";
import { isMappedPolicyCurrent } from "src/request-management/inquiry-participant-resolver";
describe("SandHubService inquiry mocks", () => { describe("SandHubService inquiry mocks", () => {
const httpService = { post: jest.fn() }; const httpService = { post: jest.fn() };
@@ -56,6 +57,12 @@ describe("SandHubService inquiry mocks", () => {
}); });
}); });
it("keeps disabled-live third-party mock policy usable", async () => {
const result = await service.getTejaratBlockInquiry(userDetail);
expect(isMappedPolicyCurrent(result.mapped)).toBe(true);
});
it("returns car-body mock without HTTP when carBodyPlate is off", async () => { it("returns car-body mock without HTTP when carBodyPlate is off", async () => {
const result = await service.getTejaratCarBodyInquiry(userDetail); const result = await service.getTejaratCarBodyInquiry(userDetail);

View File

@@ -200,7 +200,8 @@ export class SandHubService {
CompanyCode: ctx.companyId, CompanyCode: ctx.companyId,
IssueDate: "1403/04/06", IssueDate: "1403/04/06",
SatrtDate: "1403/04/06", SatrtDate: "1403/04/06",
EndDate: "1404/04/06", // Keep disabled-live mock mode stable; this fixture must not expire with wall-clock time.
EndDate: "1499/12/29",
Thrname: "", Thrname: "",
EndorseText: null, EndorseText: null,
PolicyHealthLossCount: 0, PolicyHealthLossCount: 0,