From ef9768795d7bb0899671a76796153db8a2b34b00 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sun, 6 Sep 2026 14:11:32 +0330 Subject: [PATCH] Implemented CAR_BODY fanavaran inquiry --- .../entities/schema/partyRole.enum.ts | 34 +++- .../schema/request-management.schema.ts | 9 +- .../request-management.service.ts | 187 ++++++++++++------ .../esg-car-body-inquiry.mapper.spec.ts | 65 ++++++ src/sand-hub/esg-car-body-inquiry.mapper.ts | 79 ++++++++ src/sand-hub/sand-hub.module.ts | 2 + src/sand-hub/sand-hub.service.spec.ts | 75 +++++++ src/sand-hub/sand-hub.service.ts | 83 ++++++++ 8 files changed, 470 insertions(+), 64 deletions(-) create mode 100644 src/sand-hub/esg-car-body-inquiry.mapper.spec.ts create mode 100644 src/sand-hub/esg-car-body-inquiry.mapper.ts diff --git a/src/request-management/entities/schema/partyRole.enum.ts b/src/request-management/entities/schema/partyRole.enum.ts index 82563f0..61fb12e 100644 --- a/src/request-management/entities/schema/partyRole.enum.ts +++ b/src/request-management/entities/schema/partyRole.enum.ts @@ -79,14 +79,44 @@ export class Insurance { @Prop({ type: [String] }) coverages?: string[]; - /** CAR_BODY only: mocked car-body insurance inquiry result */ + /** CAR_BODY inquiry result; raw provider data is retained in vehicle.inquiry.carBody.raw. */ @Prop({ type: MongooseSchema.Types.Mixed }) carBodyInsurance?: { policyNumber?: string; + policyId?: number; + contractId?: number; startDate?: string; endDate?: string; insurerCompany?: string; coverages?: string[]; + companyId?: number | string; + companyName?: string; + insurerName?: string; + insurerNationalCode?: string; + ownerNationalCode?: string; + ownerName?: string; + customerName?: string; + customerLastName?: string; + customerFatherName?: string; + customerMobile?: string; + customerAddress?: string; + customerPostalCode?: string; + chassisNumber?: string; + vin?: string; + motorNumber?: string; + vehicleGroup?: string; + vehicleSystem?: string; + vehicleKind?: string; + builtYear?: number; + cylinderCount?: number; + passengerCount?: number; + usage?: string; + issueDate?: string; + vehicleValue?: number; + totalPremium?: number; + noLossYearsCount?: number; + lossDocuments?: unknown[]; + hasEndorsement?: boolean; }; } export const InsuranceSchema = SchemaFactory.createForClass(Insurance); @@ -183,4 +213,4 @@ export class Party { @Prop({ type: PartyConfirmationSchema }) confirmation?: PartyConfirmation; } -export const PartySchema = SchemaFactory.createForClass(Party); \ No newline at end of file +export const PartySchema = SchemaFactory.createForClass(Party); diff --git a/src/request-management/entities/schema/request-management.schema.ts b/src/request-management/entities/schema/request-management.schema.ts index c1ad608..e6456eb 100644 --- a/src/request-management/entities/schema/request-management.schema.ts +++ b/src/request-management/entities/schema/request-management.schema.ts @@ -1,5 +1,5 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; -import { Types } from "mongoose"; +import { Schema as MongooseSchema, Types } from "mongoose"; import { ResendFirstPartyDto, ResendSecondPartyDto, @@ -79,6 +79,13 @@ export class CarBodyInsuranceDetail { @Prop({ type: [String] }) coverages?: string[]; + + /** Complete provider payload, retained for audit and future field mapping. */ + @Prop({ type: MongooseSchema.Types.Mixed }) + raw?: unknown; + + @Prop() + source?: string; } export class SecondPartyFile { diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 8016684..18ca9db 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -1513,7 +1513,7 @@ export class RequestManagementService { if (req.type === BlameRequestType.CAR_BODY && role === PartyRole.FIRST) { let carBodyInfo: any; try { - carBodyInfo = await this.sandHubService.getTejaratCarBodyInquiry( + carBodyInfo = await this.sandHubService.getCarBodyInquiry( { nationalCodeOfInsurer: body.nationalCodeOfInsurer, plate: body.plate, @@ -1523,13 +1523,13 @@ export class RequestManagementService { : inquiryOptions, ); this.recordPartyCaseInquiryStatus(req, "carBody", role, true, { - source: "TEJARAT_CAR_BODY_INQUIRY", + source: carBodyInfo.source, raw: carBodyInfo.raw, mapped: carBodyInfo.mapped, }); } catch (err: any) { this.logger.error( - `[TEJARAT] car body inquiry failed for request=${req._id}: ${err?.message || err}`, + `[CAR_BODY] inquiry failed for request=${req._id}: ${err?.message || err}`, ); this.recordPartyCaseInquiryStatus( req, @@ -1552,7 +1552,7 @@ export class RequestManagementService { party.vehicle.inquiry = { ...party.vehicle.inquiry, carBody: { - source: "TEJARAT_CAR_BODY_INQUIRY", + source: carBodyInfo.source, raw: carBodyInfo.raw, mapped: carBodyInfo.mapped, }, @@ -1562,19 +1562,35 @@ export class RequestManagementService { const m = carBodyInfo.mapped; (party.insurance as any).carBodyInsurance = { policyNumber: m.policyNumber ?? null, + policyId: m.policyId ?? null, + contractId: m.contractId ?? null, companyId: m.companyId ?? null, companyName: m.CompanyName ?? null, insurerName: m.insurerName ?? null, insurerNationalCode: m.insurerNationalCode ?? null, ownerNationalCode: m.ownerNationalCode ?? null, + ownerName: m.ownerName ?? null, + customerName: m.customerName ?? null, + customerLastName: m.customerLastName ?? null, + customerFatherName: m.customerFatherName ?? null, + customerMobile: m.customerMobile ?? null, + customerAddress: m.customerAddress ?? null, + customerPostalCode: m.customerPostalCode ?? null, chassisNumber: m.ChassisNumberField ?? null, vin: m.VinNumberField ?? null, motorNumber: m.EngineNumberField ?? null, vehicleGroup: m.vehicleGroupTitle ?? null, vehicleSystem: m.vehicleSystemTitle ?? null, + vehicleKind: m.vehicleKind ?? null, + builtYear: m.builtYear ?? null, + cylinderCount: m.cylinderCount ?? null, + passengerCount: m.passengerCount ?? null, + usage: m.usage ?? null, startDate: m.StartDate ?? null, endDate: m.EndDate ?? null, issueDate: m.IssueDate ?? null, + vehicleValue: m.vehicleValue ?? null, + totalPremium: m.totalPremium ?? null, noLossYearsCount: m.noLossYearsCount ?? null, lossDocuments: m.lossDocuments ?? [], hasEndorsement: m.hasEndorsement ?? null, @@ -3006,13 +3022,15 @@ export class RequestManagementService { sandHubReport.EndDate, }; - // For CAR_BODY type, also fetch and persist mocked car body insurance info + // For CAR_BODY type, persist the provider response and its mapped fields. if (request.type === "CAR_BODY" && partyType === "firstParty") { - const carBodyInfo = await this.mockCarBodyInsuranceInquiry( - request._id, - body.plate, - body.nationalCodeOfInsurer, + const carBodyInquiry = await this.sandHubService.getCarBodyInquiry( + { + nationalCodeOfInsurer: body.nationalCodeOfInsurer, + plate: body.plate, + } as any, ); + const carBodyInfo = carBodyInquiry.mapped as any; this.logger.log( `[CAR_BODY] Saving car body insurance data to blame file ${request._id}:`, @@ -3035,6 +3053,12 @@ export class RequestManagementService { setFields[ "firstPartyDetails.firstPartyCarBodyInsuranceDetail.coverages" ] = carBodyInfo.coverages; + setFields[ + "firstPartyDetails.firstPartyCarBodyInsuranceDetail.raw" + ] = carBodyInquiry.raw; + setFields[ + "firstPartyDetails.firstPartyCarBodyInsuranceDetail.source" + ] = carBodyInquiry.source; } // Build final update payload with proper MongoDB operators @@ -4423,40 +4447,6 @@ export class RequestManagementService { return { message: "Update saved successfully." }; } - // Mocked CAR_BODY insurance inquiry – replace with real external API later - private async mockCarBodyInsuranceInquiry( - requestId: string, - plate: any, - nationalCodeOfInsurer: string, - ): Promise<{ - policyNumber: string; - startDate: string; - endDate: string; - insurerCompany: string; - coverages: string[]; - }> { - this.logger.log( - `[CAR_BODY] Mocking car body insurance inquiry for request ${requestId} (plate=${JSON.stringify( - plate, - )}, nationalCodeOfInsurer=${nationalCodeOfInsurer})`, - ); - - const today = new Date(); - const oneYearLater = new Date( - today.getFullYear() + 1, - today.getMonth(), - today.getDate(), - ); - - return { - policyNumber: "CB-MOCK-123456", - startDate: today.toISOString().slice(0, 10), - endDate: oneYearLater.toISOString().slice(0, 10), - insurerCompany: "Mock Car Body Insurance Co.", - coverages: ["آتش‌سوزی", "سرقت", "بدنه کامل"], - }; - } - private async processResendUploads( files: { [key: string]: Express.Multer.File[] }, requestId: string, @@ -6325,17 +6315,21 @@ export class RequestManagementService { }); } - const carBodyInfo = await this.mockCarBodyInsuranceInquiry( - requestId, - firstPartyPlate.plate, - firstPartyPlate.nationalCodeOfInsurer, - ); + const carBodyInquiry = await this.sandHubService.getCarBodyInquiry({ + nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer, + plate: firstPartyPlate.plate, + } as any); + const carBodyInfo = carBodyInquiry.mapped as any; this.recordPartyCaseInquiryStatus( req, "carBody", PartyRole.FIRST, true, - carBodyInfo, + { + source: carBodyInquiry.source, + raw: carBodyInquiry.raw, + mapped: carBodyInfo, + }, ); const firstParty: any = { @@ -6364,7 +6358,14 @@ export class RequestManagementService { model: sandHubReport?.MapTypNam, type: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`, isNew: firstPartyPlate.isNewCar, - inquiry: sandHubReport, + inquiry: { + ...sandHubReport, + carBody: { + source: carBodyInquiry.source, + raw: carBodyInquiry.raw, + mapped: carBodyInfo, + }, + }, }, insurance: { policyNumber: @@ -6381,6 +6382,8 @@ export class RequestManagementService { endDate: carBodyInfo.endDate, insurerCompany: carBodyInfo.insurerCompany, coverages: carBodyInfo.coverages, + raw: carBodyInquiry.raw, + source: carBodyInquiry.source, }, }, statement: { @@ -7585,11 +7588,11 @@ export class RequestManagementService { }; // CAR_BODY specific insurance info - const carBodyInfo = await this.mockCarBodyInsuranceInquiry( - request._id.toString(), - firstPartyPlate.plate, - firstPartyPlate.nationalCodeOfInsurer, - ); + const carBodyInquiry = await this.sandHubService.getCarBodyInquiry({ + nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer, + plate: firstPartyPlate.plate, + } as any); + const carBodyInfo = carBodyInquiry.mapped as any; firstPartyDetails.firstPartyCarBodyInsuranceDetail = { policyNumber: carBodyInfo.policyNumber, @@ -7597,6 +7600,8 @@ export class RequestManagementService { endDate: carBodyInfo.endDate, insurerCompany: carBodyInfo.insurerCompany, coverages: carBodyInfo.coverages, + raw: carBodyInquiry.raw, + source: carBodyInquiry.source, }; // Update the firstPartyDetails in the payload with all plate data @@ -8808,7 +8813,7 @@ export class RequestManagementService { partyRole === PartyRole.FIRST ) { try { - const carBodyInfo = await this.sandHubService.getTejaratCarBodyInquiry( + const carBodyInfo = await this.sandHubService.getCarBodyInquiry( { nationalCodeOfInsurer: partyData.nationalCodeOfInsurer, plate: partyData.plate as any, @@ -8816,14 +8821,14 @@ export class RequestManagementService { clientId ? { clientId } : undefined, ); this.recordPartyCaseInquiryStatus(req, "carBody", partyRole, true, { - source: "TEJARAT_CAR_BODY_INQUIRY", + source: carBodyInfo.source, raw: carBodyInfo.raw, mapped: carBodyInfo.mapped, }); party.vehicle.inquiry = { ...party.vehicle.inquiry, carBody: { - source: "TEJARAT_CAR_BODY_INQUIRY", + source: carBodyInfo.source, raw: carBodyInfo.raw, mapped: carBodyInfo.mapped, }, @@ -8831,8 +8836,38 @@ export class RequestManagementService { const m = carBodyInfo.mapped; (party.insurance as any).carBodyInsurance = { policyNumber: m.policyNumber ?? null, + policyId: m.policyId ?? null, + contractId: m.contractId ?? null, companyId: m.companyId ?? null, companyName: m.CompanyName ?? null, + insurerName: m.insurerName ?? null, + insurerNationalCode: m.insurerNationalCode ?? null, + ownerNationalCode: m.ownerNationalCode ?? null, + ownerName: m.ownerName ?? null, + customerName: m.customerName ?? null, + customerLastName: m.customerLastName ?? null, + customerFatherName: m.customerFatherName ?? null, + customerMobile: m.customerMobile ?? null, + customerAddress: m.customerAddress ?? null, + customerPostalCode: m.customerPostalCode ?? null, + chassisNumber: m.ChassisNumberField ?? null, + vin: m.VinNumberField ?? null, + motorNumber: m.EngineNumberField ?? null, + vehicleGroup: m.vehicleGroupTitle ?? null, + vehicleSystem: m.vehicleSystemTitle ?? null, + vehicleKind: m.vehicleKind ?? null, + builtYear: m.builtYear ?? null, + cylinderCount: m.cylinderCount ?? null, + passengerCount: m.passengerCount ?? null, + usage: m.usage ?? null, + startDate: m.StartDate ?? null, + endDate: m.EndDate ?? null, + issueDate: m.IssueDate ?? null, + vehicleValue: m.vehicleValue ?? null, + totalPremium: m.totalPremium ?? null, + noLossYearsCount: m.noLossYearsCount ?? null, + lossDocuments: m.lossDocuments ?? [], + hasEndorsement: m.hasEndorsement ?? null, }; const cbCompanyCode = m.companyId ?? m.CompanyCode; const cbCompanyName = m.CompanyName ?? m.companyPersianName; @@ -9633,7 +9668,7 @@ export class RequestManagementService { partyRole === PartyRole.FIRST ) { try { - const carBodyInfo = await this.sandHubService.getTejaratCarBodyInquiry( + const carBodyInfo = await this.sandHubService.getCarBodyInquiry( { nationalCodeOfInsurer: partyData.nationalCodeOfInsurer, plate: partyData.vin as any, // VIN used as identifier for CAR_BODY @@ -9641,14 +9676,14 @@ export class RequestManagementService { clientId ? { clientId } : undefined, ); this.recordPartyCaseInquiryStatus(req, "carBody", partyRole, true, { - source: "TEJARAT_CAR_BODY_VIN_INQUIRY", + source: carBodyInfo.source, raw: carBodyInfo.raw, mapped: carBodyInfo.mapped, }); party.vehicle.inquiry = { ...party.vehicle.inquiry, carBody: { - source: "TEJARAT_CAR_BODY_VIN_INQUIRY", + source: carBodyInfo.source, raw: carBodyInfo.raw, mapped: carBodyInfo.mapped, }, @@ -9656,8 +9691,38 @@ export class RequestManagementService { const m = carBodyInfo.mapped; (party.insurance as any).carBodyInsurance = { policyNumber: m.policyNumber ?? null, + policyId: m.policyId ?? null, + contractId: m.contractId ?? null, companyId: m.companyId ?? null, companyName: m.CompanyName ?? null, + insurerName: m.insurerName ?? null, + insurerNationalCode: m.insurerNationalCode ?? null, + ownerNationalCode: m.ownerNationalCode ?? null, + ownerName: m.ownerName ?? null, + customerName: m.customerName ?? null, + customerLastName: m.customerLastName ?? null, + customerFatherName: m.customerFatherName ?? null, + customerMobile: m.customerMobile ?? null, + customerAddress: m.customerAddress ?? null, + customerPostalCode: m.customerPostalCode ?? null, + chassisNumber: m.ChassisNumberField ?? null, + vin: m.VinNumberField ?? null, + motorNumber: m.EngineNumberField ?? null, + vehicleGroup: m.vehicleGroupTitle ?? null, + vehicleSystem: m.vehicleSystemTitle ?? null, + vehicleKind: m.vehicleKind ?? null, + builtYear: m.builtYear ?? null, + cylinderCount: m.cylinderCount ?? null, + passengerCount: m.passengerCount ?? null, + usage: m.usage ?? null, + startDate: m.StartDate ?? null, + endDate: m.EndDate ?? null, + issueDate: m.IssueDate ?? null, + vehicleValue: m.vehicleValue ?? null, + totalPremium: m.totalPremium ?? null, + noLossYearsCount: m.noLossYearsCount ?? null, + lossDocuments: m.lossDocuments ?? [], + hasEndorsement: m.hasEndorsement ?? null, }; const cbCompanyCode = m.companyId ?? m.CompanyCode; const cbCompanyName = m.CompanyName ?? m.companyPersianName; diff --git a/src/sand-hub/esg-car-body-inquiry.mapper.spec.ts b/src/sand-hub/esg-car-body-inquiry.mapper.spec.ts new file mode 100644 index 0000000..c9cce31 --- /dev/null +++ b/src/sand-hub/esg-car-body-inquiry.mapper.spec.ts @@ -0,0 +1,65 @@ +import { mapEsgCarBodyPolicyToInquiry } from "./esg-car-body-inquiry.mapper"; + +describe("mapEsgCarBodyPolicyToInquiry", () => { + it("maps the processed CAR_BODY lookup while retaining useful policy, customer, and vehicle fields", () => { + const mapped = mapEsgCarBodyPolicyToInquiry({ + product: "car-body", + insuranceLine: "CAR_BODY", + insuranceLineId: 4, + policyId: 15292336, + policy: { + CINumber: "70019846985", + ContractId: 12525, + BeginDate: "1405/05/18", + EndDate: "1406/05/18", + PolicyIssuDate: "1405/05/18", + VehicleValue: 18510000000, + TotalPremium: 73299600, + EndoNo: 0, + }, + customer: { + Name: "سهيل", + LastName: "حاجي زاده", + NationalCode: "4311402422", + Mobile: "09226187419", + }, + vehicle: { + BuiltYear: 1403, + ChassisNo: "NAAR03HFFRDE07024", + MotorNo: "172B0222818", + VIN: "NAAR03HFFRDE07024", + vehicleKind: { + Caption: "پژو 207I", + VehicleSystemCaption: "پژو", + CylinderCount: 4, + PassengerCount: 5, + }, + used: { Caption: "شخصي" }, + plaque: { + leftTwoDigits: "29", + serialLetter: "د", + threeDigits: "782", + rightTwoDigits: "60", + }, + }, + }); + + expect(mapped).toMatchObject({ + policyNumber: "70019846985", + policyId: 15292336, + contractId: 12525, + insurerName: null, + insurerNationalCode: null, + ownerName: "سهيل حاجي زاده", + ownerNationalCode: "4311402422", + VinNumberField: "NAAR03HFFRDE07024", + MapTypNam: "پژو 207I", + vehicleValue: 18510000000, + totalPremium: 73299600, + platePartOne: "29", + plateLetterTitle: "د", + platePartThree: "782", + plateSerialNumber: "60", + }); + }); +}); diff --git a/src/sand-hub/esg-car-body-inquiry.mapper.ts b/src/sand-hub/esg-car-body-inquiry.mapper.ts new file mode 100644 index 0000000..1a3a720 --- /dev/null +++ b/src/sand-hub/esg-car-body-inquiry.mapper.ts @@ -0,0 +1,79 @@ +/** + * Converts the processed CAR_BODY policy returned by the ESG/Fanavaran lookup + * into the same mapped contract used by the existing car-body inquiry flow. + * The original response is deliberately kept separately as `raw` by the + * caller, so newly exposed provider fields are never discarded. + */ +export function mapEsgCarBodyPolicyToInquiry( + response: Record, +): Record { + const policy = response.policy ?? {}; + const customer = response.customer ?? {}; + const vehicle = response.vehicle ?? {}; + const vehicleKind = vehicle.vehicleKind ?? {}; + const usage = vehicle.used ?? {}; + const plaque = vehicle.plaque ?? {}; + + const ownerName = + [customer.Name, customer.LastName] + .filter((value) => typeof value === "string" && value.trim()) + .join(" ") || null; + + return { + policyNumber: policy.CINumber ?? policy.PolicyNo ?? null, + policyId: response.policyId ?? policy.PolicyId ?? null, + contractId: policy.ContractId ?? null, + insuranceLine: response.insuranceLine ?? "CAR_BODY", + insuranceLineId: response.insuranceLineId ?? 4, + + // Provider responses do not currently include an insurer company identity. + companyId: policy.CompanyId ?? null, + CompanyCode: policy.CompanyId ?? null, + CompanyName: policy.CompanyName ?? null, + + // The lookup's customer is the policyholder/owner, not the insurer. + insurerName: null, + InsuranceFullName: null, + insurerNationalCode: null, + ownerNationalCode: customer.NationalCode ?? null, + ownerName, + customerName: customer.Name ?? null, + customerLastName: customer.LastName ?? null, + customerFatherName: customer.FatherName ?? null, + customerMobile: customer.Mobile ?? null, + customerAddress: customer.Address ?? null, + customerPostalCode: customer.PostalCode ?? null, + + motorNumber: vehicle.MotorNo ?? null, + EngineNumberField: vehicle.MotorNo ?? null, + chassisNumber: vehicle.ChassisNo ?? null, + ChassisNumberField: vehicle.ChassisNo ?? null, + vin: vehicle.VIN ?? vehicle.ChassisNo ?? null, + VinNumberField: vehicle.VIN ?? vehicle.ChassisNo ?? null, + vehicleGroupTitle: vehicleKind.VehicleCategoryCaption ?? null, + vehicleSystemTitle: vehicleKind.VehicleSystemCaption ?? null, + vehicleKind: vehicleKind.Caption ?? null, + MapTypNam: vehicleKind.Caption ?? vehicleKind.VehicleSystemCaption ?? null, + builtYear: vehicle.BuiltYear ?? null, + cylinderCount: vehicleKind.CylinderCount ?? null, + passengerCount: vehicleKind.PassengerCount ?? null, + usage: usage.Caption ?? null, + + IssueDate: policy.PolicyIssuDate ?? policy.IssuDate ?? null, + StartDate: policy.BeginDate ?? null, + EndDate: policy.EndDate ?? null, + vehicleValue: policy.VehicleValue ?? null, + totalPremium: policy.TotalPremium ?? null, + noLossYearsCount: + policy.YearsCountWithoutLoss ?? + policy.AdditionalCovYearsCountWithoutLoss ?? + null, + lossDocuments: [], + hasEndorsement: Number(policy.EndoNo ?? 0) > 0, + + platePartOne: plaque.leftTwoDigits ?? vehicle.PlaqueLeftNo ?? null, + plateLetterTitle: plaque.serialLetter ?? null, + platePartThree: plaque.threeDigits ?? vehicle.PlaqueRightNo ?? null, + plateSerialNumber: plaque.rightTwoDigits ?? vehicle.PlaqueSerial ?? null, + }; +} diff --git a/src/sand-hub/sand-hub.module.ts b/src/sand-hub/sand-hub.module.ts index 2385780..f02c30b 100644 --- a/src/sand-hub/sand-hub.module.ts +++ b/src/sand-hub/sand-hub.module.ts @@ -7,6 +7,7 @@ import { ClientModule } from "src/client/client.module"; import { SystemSettingsModule } from "src/system-settings/system-settings.module"; import { PlateNormalizerModule } from "src/utils/plate-normalizer/plate-normalizer.module"; import { OfflineInquiryModule } from "src/offline-inquiry/offline-inquiry.module"; +import { LookupsModule } from "src/lookups/lookups.module"; import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service"; import { SandHubModel, SandHubSchema } from "./entity/schema/sand-hub.schema"; import { SandHubService } from "./sand-hub.service"; @@ -22,6 +23,7 @@ import { SandHubService } from "./sand-hub.service"; PlateNormalizerModule, ClientModule, OfflineInquiryModule, + LookupsModule, MongooseModule.forFeature([ { name: SandHubModel.name, schema: SandHubSchema }, ]), diff --git a/src/sand-hub/sand-hub.service.spec.ts b/src/sand-hub/sand-hub.service.spec.ts index 284b02e..915623a 100644 --- a/src/sand-hub/sand-hub.service.spec.ts +++ b/src/sand-hub/sand-hub.service.spec.ts @@ -12,6 +12,9 @@ describe("SandHubService inquiry mocks", () => { const plateNormalizer = { normalizePlateText: (text: string) => text, }; + const lookupsService = { + findLastProcessedCarPolicy: jest.fn(), + }; let service: SandHubService; @@ -37,6 +40,7 @@ describe("SandHubService inquiry mocks", () => { { findPlateInquiry: jest.fn().mockResolvedValue(null), } as any, + lookupsService as any, ); externalInquirySettings.isInquiryLive.mockResolvedValue(false); externalInquirySettings.getMockCompanyContext.mockResolvedValue({ @@ -64,6 +68,77 @@ describe("SandHubService inquiry mocks", () => { expect(result.mapped.platePartThree).toBe(498); }); + it("uses the processed ESG CAR_BODY lookup and preserves it as raw data", async () => { + process.env.CLIENT_ID = "8"; + externalInquirySettings.isInquiryLive.mockResolvedValue(true); + lookupsService.findLastProcessedCarPolicy.mockResolvedValue({ + product: "car-body", + insuranceLine: "CAR_BODY", + insuranceLineId: 4, + policyId: 15292336, + policy: { CINumber: "70019846985", BeginDate: "1405/05/18" }, + customer: { Name: "سهيل", LastName: "حاجي زاده" }, + vehicle: { + VIN: "NAAR03HFFRDE07024", + plaque: { + leftTwoDigits: "16", + serialLetter: "ب", + threeDigits: "498", + rightTwoDigits: "60", + }, + }, + }); + + const result = await service.getCarBodyInquiry(userDetail); + + expect(lookupsService.findLastProcessedCarPolicy).toHaveBeenCalledWith( + "car-body", + { + nationalCode: "1234567890", + plaqueLeft: "16", + plaqueLetter: "12", + plaqueRight: "498", + plaqueSerial: "60", + }, + ); + expect(result.source).toBe("ESG_CAR_BODY_INQUIRY"); + expect(result.raw.policyId).toBe(15292336); + expect(result.mapped.policyNumber).toBe("70019846985"); + expect(httpService.post).not.toHaveBeenCalled(); + }); + + it("keeps ESG car-body inquiry in mock mode when the per-client toggle is off", async () => { + process.env.CLIENT_ID = "8"; + + const result = await service.getCarBodyInquiry(userDetail); + + expect(lookupsService.findLastProcessedCarPolicy).not.toHaveBeenCalled(); + expect(result.source).toBe("ESG_CAR_BODY_INQUIRY"); + expect(result.raw?.isSuccess).toBe(true); + expect(httpService.post).not.toHaveBeenCalled(); + }); + + it("uses the VIN lookup and VIN audit source for ESG car-body VIN inquiries", async () => { + process.env.CLIENT_ID = "8"; + externalInquirySettings.isInquiryLive.mockResolvedValue(true); + lookupsService.findLastProcessedCarPolicy.mockResolvedValue({ + policy: { CINumber: "70019846985" }, + customer: {}, + vehicle: {}, + }); + + const result = await service.getCarBodyInquiry({ + nationalCodeOfInsurer: "1234567890", + plate: "NAAR03HFFRDE07024", + }); + + expect(lookupsService.findLastProcessedCarPolicy).toHaveBeenCalledWith( + "car-body", + { nationalCode: "1234567890", vin: "NAAR03HFFRDE07024" }, + ); + expect(result.source).toBe("ESG_CAR_BODY_VIN_INQUIRY"); + }); + it("uses car-body mock shape in Tejarat helper when inquiry is off", async () => { const raw = await (service as any).makeTejaratRequest( "http://example/block-inquiry-tejarat/badane", diff --git a/src/sand-hub/sand-hub.service.ts b/src/sand-hub/sand-hub.service.ts index 6f90fb2..28b2f8a 100644 --- a/src/sand-hub/sand-hub.service.ts +++ b/src/sand-hub/sand-hub.service.ts @@ -16,10 +16,17 @@ import { PlateNormalizerService } from "src/utils/plate-normalizer/plate-normali import type { ExternalInquiryType } from "src/common/types/external-inquiry.types"; import type { MockInquiryCompanyContext } from "src/common/types/external-inquiry.types"; import { OfflineInquiryService } from "src/offline-inquiry/offline-inquiry.service"; +import { LookupsService } from "src/lookups/lookups.service"; import { resolveFanavaranClientKey } from "src/core/config/fanavaran-client.config"; import { SandHubDetailDto, SandHubInquiryOptions } from "./dto/sand-hub.dto"; import { jalaliToGregorianDate } from "src/helpers/date-jalali"; import { firstValueFrom } from "rxjs"; +import { mapEsgCarBodyPolicyToInquiry } from "./esg-car-body-inquiry.mapper"; +import type { Plates } from "src/Types&Enums/plate.interface"; + +type CarBodyInquiryDetail = Omit & { + plate: Plates | string; +}; @Injectable() export class SandHubService { @@ -44,6 +51,7 @@ export class SandHubService { private readonly externalInquirySettings: ExternalInquirySettingsService, private readonly plateNormalizer: PlateNormalizerService, private readonly offlineInquiryService: OfflineInquiryService, + private readonly lookupsService: LookupsService, ) {} private clientRefFrom(options?: SandHubInquiryOptions): string | undefined { @@ -833,6 +841,81 @@ export class SandHubService { return await this.sandHubDbService.findOneBySandHubId(sandHubId); } + /** + * CAR_BODY is the only ESG product that uses the processed Fanavaran lookup. + * THIRD_PARTY remains on its established provider-specific inquiry path. + */ + async getCarBodyInquiry( + userDetail: CarBodyInquiryDetail, + options?: SandHubInquiryOptions, + ): Promise<{ + source: + | "ESG_CAR_BODY_INQUIRY" + | "ESG_CAR_BODY_VIN_INQUIRY" + | "TEJARAT_CAR_BODY_INQUIRY" + | "TEJARAT_CAR_BODY_VIN_INQUIRY"; + raw: any; + mapped: Record; + }> { + if (!this.shouldUseEsgInquiryProvider()) { + const result = await this.getTejaratCarBodyInquiry( + userDetail as SandHubDetailDto, + options, + ); + return { + source: + typeof userDetail.plate === "string" + ? "TEJARAT_CAR_BODY_VIN_INQUIRY" + : "TEJARAT_CAR_BODY_INQUIRY", + ...result, + }; + } + + const live = await this.isInquiryLive("carBodyPlate", options); + if (!live) { + // Reuse the established local mock contract without sending an HTTP request. + const result = await this.getTejaratCarBodyInquiry( + userDetail as SandHubDetailDto, + options, + ); + return { + source: + typeof userDetail.plate === "string" + ? "ESG_CAR_BODY_VIN_INQUIRY" + : "ESG_CAR_BODY_INQUIRY", + ...result, + }; + } + + const plateOrVin = userDetail.plate; + const query = + typeof plateOrVin === "string" + ? { + nationalCode: String(userDetail.nationalCodeOfInsurer), + vin: plateOrVin, + } + : { + nationalCode: String(userDetail.nationalCodeOfInsurer), + plaqueLeft: String(plateOrVin.leftDigits), + plaqueLetter: String(plateOrVin.centerAlphabet), + plaqueRight: String(plateOrVin.centerDigits), + plaqueSerial: String(plateOrVin.ir), + }; + const raw = await this.lookupsService.findLastProcessedCarPolicy( + "car-body", + query, + ); + + return { + source: + typeof plateOrVin === "string" + ? "ESG_CAR_BODY_VIN_INQUIRY" + : "ESG_CAR_BODY_INQUIRY", + raw, + mapped: mapEsgCarBodyPolicyToInquiry(raw), + }; + } + async getTejaratCarBodyInquiry( userDetail: SandHubDetailDto, options?: SandHubInquiryOptions,