diff --git a/src/request-management/inquiry-refresh.service.spec.ts b/src/request-management/inquiry-refresh.service.spec.ts new file mode 100644 index 0000000..0276dca --- /dev/null +++ b/src/request-management/inquiry-refresh.service.spec.ts @@ -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(); + }); +}); diff --git a/src/request-management/inquiry-refresh.service.ts b/src/request-management/inquiry-refresh.service.ts index 40d669f..e9e0776 100644 --- a/src/request-management/inquiry-refresh.service.ts +++ b/src/request-management/inquiry-refresh.service.ts @@ -18,6 +18,10 @@ import { ReinquiryPartyResultDto, } from "./dto/reinquiry-inquiries.dto"; 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 = { leftDigits: number; @@ -193,10 +197,10 @@ export class InquiryRefreshService { let partyChanged = 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 nationalCode = - this.cleanString(party?.person?.nationalCodeOfInsurer) || - this.cleanString(party?.person?.nationalCodeOfDriver); + const nationalCode = this.resolveCurrentPolicyholderNationalCode(party); const birthDate = party?.person?.insurerBirthday ?? party?.person?.driverBirthday ?? @@ -619,12 +623,14 @@ export class InquiryRefreshService { ); if (fromPlateId) return fromPlateId; - const candidates = [ - party?.vehicle?.inquiry?.mapped, - party?.vehicle?.inquiry?.raw, - party?.vehicle?.inquiry, - party?.vehicle, - ].filter(Boolean); + // A missing plateId is intentionally not recovered from an inquiry + // response: that response may belong to a previous plate on transferred + // vehicles. The refresh operation must fail closed instead of mixing + // identifiers. + const currentPlate = party?.vehicle?.currentPlate; + if (!currentPlate || typeof currentPlate !== "object") return null; + + const candidates = [currentPlate]; for (const candidate of candidates) { const leftDigits = this.firstPresent( @@ -675,6 +681,19 @@ export class InquiryRefreshService { return null; } + private resolveCurrentPolicyholderNationalCode( + party: Record, + ): 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 { if (!plateId) return null; const parts = String(plateId).split("-"); diff --git a/src/request-management/request-management.policyholder-inquiry.spec.ts b/src/request-management/request-management.policyholder-inquiry.spec.ts index bd1b974..daefd17 100644 --- a/src/request-management/request-management.policyholder-inquiry.spec.ts +++ b/src/request-management/request-management.policyholder-inquiry.spec.ts @@ -215,7 +215,7 @@ describe("RequestManagementService policyholder inquiry routing", () => { expect(service.getThirdPartyVinInquiry).toHaveBeenCalledTimes(1); expect(service.sandHubService.getCarBodyInquiry).toHaveBeenCalledWith({ nationalCodeOfInsurer: "0033333333", - plate: vehicle.vin, + plate: currentPlate, }); expect(req.inquiries.thirdParty.has).toBe(true); expect(req.inquiries.carBody.has).toBe(true); diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 63becba..b48d4d5 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -14,6 +14,7 @@ import { ExternalExceptionFilter } from "@nestjs/core/exceptions/external-except import { Types } from "mongoose"; import { ClientService } from "src/client/client.service"; import { AddPlateDto } from "src/profile/dto/user/AddPlateDto"; +import { VehicleRegistrationState } from "src/common/dto/inquiry-participants.dto"; import { CarBodyFormDto, 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>, + ): Promise { + 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( submission: NormalizedInquirySubmission>, options?: Record, @@ -2519,10 +2543,8 @@ export class RequestManagementService { // flow before the case is allowed to advance. if (req.type === BlameRequestType.CAR_BODY && role === PartyRole.FIRST) { try { - const carBodyInfo = await this.sandHubService.getCarBodyInquiry({ - nationalCodeOfInsurer: subjects.carBodyPolicyNationalCode!, - plate: body.vin, - }); + const carBodyInfo = + await this.getCarBodyInquiryForVinSubmission(inquirySubmission); this.recordPartyCaseInquiryStatus(req, "carBody", role, true, { source: carBodyInfo.source, raw: carBodyInfo.raw, @@ -11117,10 +11139,8 @@ export class RequestManagementService { partyRole === PartyRole.FIRST ) { try { - const carBodyInfo = await this.sandHubService.getCarBodyInquiry({ - nationalCodeOfInsurer: subjects.carBodyPolicyNationalCode!, - plate: partyData.vin as any, // VIN used as identifier for CAR_BODY - }); + const carBodyInfo = + await this.getCarBodyInquiryForVinSubmission(inquirySubmission); this.recordPartyCaseInquiryStatus(req, "carBody", partyRole, true, { source: carBodyInfo.source, raw: carBodyInfo.raw,