Merge pull request 'Fix transferred vehicle inquiry routing' (#342) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#342
This commit is contained in:
2026-09-28 00:53:24 +03:30
4 changed files with 125 additions and 18 deletions

View File

@@ -0,0 +1,68 @@
import { InquiryRefreshService } from "./inquiry-refresh.service";
describe("InquiryRefreshService current-identifier routing", () => {
const createService = () => {
const service = Object.create(InquiryRefreshService.prototype) as any;
service.plateNormalizer = {
normalizePlateText: (value: string) => value,
};
service.cleanString = (value: unknown) =>
value === undefined || value === null ? "" : String(value).trim();
return service;
};
it("uses the participant-linked current policyholder, not transfer metadata", () => {
const service = createService();
const party = {
participants: [
{
participantId: "THIRD_PARTY_POLICYHOLDER",
nationalCode: "0011111111",
},
],
participantRoles: {
thirdPartyPolicyholder: "THIRD_PARTY_POLICYHOLDER",
},
person: {
nationalCodeOfInsurer: "0099999999",
nationalCodeOfDriver: "0088888888",
},
vehicle: {
registrationState: "RECENTLY_TRANSFERRED",
plateId: "22-44-ب-111",
previousPlateId: "33-55-ج-222",
previousPolicyholderNationalCode: "0077777777",
},
};
expect(service.resolveCurrentPolicyholderNationalCode(party)).toBe(
"0011111111",
);
expect(service.resolvePartyPlate(party)).toEqual({
ir: 22,
leftDigits: 44,
centerAlphabet: "ب",
centerDigits: 111,
});
});
it("fails closed instead of rebuilding a plate from an old inquiry payload", () => {
const service = createService();
const party = {
vehicle: {
registrationState: "RECENTLY_TRANSFERRED",
previousPlateId: "33-55-ج-222",
inquiry: {
mapped: {
Plk1: 55,
Plk2: "ج",
Plk3: 222,
PlkSrl: 33,
},
},
},
};
expect(service.resolvePartyPlate(party)).toBeNull();
});
});

View File

@@ -18,6 +18,10 @@ import {
ReinquiryPartyResultDto, ReinquiryPartyResultDto,
} from "./dto/reinquiry-inquiries.dto"; } from "./dto/reinquiry-inquiries.dto";
import { getInquiryErrorMessage } from "src/common/utils/inquiry-error"; import { getInquiryErrorMessage } from "src/common/utils/inquiry-error";
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
import {
participantForStoredPartyRole,
} from "./inquiry-participant-resolver";
type PlateParts = { type PlateParts = {
leftDigits: number; leftDigits: number;
@@ -193,10 +197,10 @@ export class InquiryRefreshService {
let partyChanged = false; let partyChanged = false;
let inquiriesChanged = false; let inquiriesChanged = false;
// Re-inquiry must use the canonical current identifiers only. Previous
// transfer metadata and old provider payloads are never valid lookup input.
const plate = this.resolvePartyPlate(party); const plate = this.resolvePartyPlate(party);
const nationalCode = const nationalCode = this.resolveCurrentPolicyholderNationalCode(party);
this.cleanString(party?.person?.nationalCodeOfInsurer) ||
this.cleanString(party?.person?.nationalCodeOfDriver);
const birthDate = const birthDate =
party?.person?.insurerBirthday ?? party?.person?.insurerBirthday ??
party?.person?.driverBirthday ?? party?.person?.driverBirthday ??
@@ -619,12 +623,14 @@ export class InquiryRefreshService {
); );
if (fromPlateId) return fromPlateId; if (fromPlateId) return fromPlateId;
const candidates = [ // A missing plateId is intentionally not recovered from an inquiry
party?.vehicle?.inquiry?.mapped, // response: that response may belong to a previous plate on transferred
party?.vehicle?.inquiry?.raw, // vehicles. The refresh operation must fail closed instead of mixing
party?.vehicle?.inquiry, // identifiers.
party?.vehicle, const currentPlate = party?.vehicle?.currentPlate;
].filter(Boolean); if (!currentPlate || typeof currentPlate !== "object") return null;
const candidates = [currentPlate];
for (const candidate of candidates) { for (const candidate of candidates) {
const leftDigits = this.firstPresent( const leftDigits = this.firstPresent(
@@ -675,6 +681,19 @@ export class InquiryRefreshService {
return null; return null;
} }
private resolveCurrentPolicyholderNationalCode(
party: Record<string, any>,
): string {
const participant = participantForStoredPartyRole(
party,
InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER,
);
return (
this.cleanString(participant?.nationalCode) ||
this.cleanString(party?.person?.nationalCodeOfInsurer)
);
}
private parsePlateFromCompactString(plateId?: string): PlateParts | null { private parsePlateFromCompactString(plateId?: string): PlateParts | null {
if (!plateId) return null; if (!plateId) return null;
const parts = String(plateId).split("-"); const parts = String(plateId).split("-");

View File

@@ -215,7 +215,7 @@ describe("RequestManagementService policyholder inquiry routing", () => {
expect(service.getThirdPartyVinInquiry).toHaveBeenCalledTimes(1); expect(service.getThirdPartyVinInquiry).toHaveBeenCalledTimes(1);
expect(service.sandHubService.getCarBodyInquiry).toHaveBeenCalledWith({ expect(service.sandHubService.getCarBodyInquiry).toHaveBeenCalledWith({
nationalCodeOfInsurer: "0033333333", nationalCodeOfInsurer: "0033333333",
plate: vehicle.vin, plate: currentPlate,
}); });
expect(req.inquiries.thirdParty.has).toBe(true); expect(req.inquiries.thirdParty.has).toBe(true);
expect(req.inquiries.carBody.has).toBe(true); expect(req.inquiries.carBody.has).toBe(true);

View File

@@ -14,6 +14,7 @@ import { ExternalExceptionFilter } from "@nestjs/core/exceptions/external-except
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ClientService } from "src/client/client.service"; import { ClientService } from "src/client/client.service";
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto"; import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
import { VehicleRegistrationState } from "src/common/dto/inquiry-participants.dto";
import { import {
CarBodyFormDto, CarBodyFormDto,
CarBodySecondForm, CarBodySecondForm,
@@ -466,6 +467,29 @@ export class RequestManagementService {
}; };
} }
/**
* A VIN submission still carries the canonical current plate. For a recent
* transfer, the car-body policy must be checked against that current plate
* and the current policyholder; the previous plate is historical metadata
* only. For ordinary VIN submissions we retain the dedicated VIN lookup.
*/
private async getCarBodyInquiryForVinSubmission(
submission: NormalizedInquirySubmission<Record<string, any>>,
): Promise<any> {
if (
submission.vehicle?.registrationState ===
VehicleRegistrationState.RECENTLY_TRANSFERRED
) {
return this.getCarBodyPlateInquiry(submission);
}
const subjects = resolveInquirySubjects(submission);
return this.sandHubService.getCarBodyInquiry({
nationalCodeOfInsurer: subjects.carBodyPolicyNationalCode!,
plate: submission.vehicle?.vin as any,
});
}
private async getThirdPartyVinInquiry( private async getThirdPartyVinInquiry(
submission: NormalizedInquirySubmission<Record<string, any>>, submission: NormalizedInquirySubmission<Record<string, any>>,
options?: Record<string, any>, options?: Record<string, any>,
@@ -2519,10 +2543,8 @@ export class RequestManagementService {
// flow before the case is allowed to advance. // flow before the case is allowed to advance.
if (req.type === BlameRequestType.CAR_BODY && role === PartyRole.FIRST) { if (req.type === BlameRequestType.CAR_BODY && role === PartyRole.FIRST) {
try { try {
const carBodyInfo = await this.sandHubService.getCarBodyInquiry({ const carBodyInfo =
nationalCodeOfInsurer: subjects.carBodyPolicyNationalCode!, await this.getCarBodyInquiryForVinSubmission(inquirySubmission);
plate: body.vin,
});
this.recordPartyCaseInquiryStatus(req, "carBody", role, true, { this.recordPartyCaseInquiryStatus(req, "carBody", role, true, {
source: carBodyInfo.source, source: carBodyInfo.source,
raw: carBodyInfo.raw, raw: carBodyInfo.raw,
@@ -11117,10 +11139,8 @@ export class RequestManagementService {
partyRole === PartyRole.FIRST partyRole === PartyRole.FIRST
) { ) {
try { try {
const carBodyInfo = await this.sandHubService.getCarBodyInquiry({ const carBodyInfo =
nationalCodeOfInsurer: subjects.carBodyPolicyNationalCode!, await this.getCarBodyInquiryForVinSubmission(inquirySubmission);
plate: partyData.vin as any, // VIN used as identifier for CAR_BODY
});
this.recordPartyCaseInquiryStatus(req, "carBody", partyRole, true, { this.recordPartyCaseInquiryStatus(req, "carBody", partyRole, true, {
source: carBodyInfo.source, source: carBodyInfo.source,
raw: carBodyInfo.raw, raw: carBodyInfo.raw,