fix inquiry integration and insurer case details

This commit is contained in:
SepehrYahyaee
2026-09-19 16:06:04 +03:30
parent cd36a0f2d4
commit 8071215803
18 changed files with 141 additions and 167 deletions

View File

@@ -58,7 +58,6 @@ import { MediaPolicyModule } from "src/media-policy/media-policy.module";
import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module";
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
import { PlateNormalizerModule } from "src/utils/plate-normalizer/plate-normalizer.module";
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
@Module({
imports: [
@@ -77,7 +76,6 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
SandHubModule,
ClientModule,
MediaPolicyModule,
SmsOrchestrationModule,
JwtModule.register({}),
MongooseModule.forFeature([
{ name: ClaimCase.name, schema: ClaimCaseSchema },

View File

@@ -249,7 +249,6 @@ import {
FanavaranAuditStatus,
FanavaranAuditStep,
} from "src/fanavaran/schema/fanavaran-audit-log.schema";
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection";
import {
selectAccidentVehicleUsedId,
@@ -487,7 +486,6 @@ export class ClaimRequestManagementService {
private readonly fanavaranAuditService: FanavaranAuditService,
private readonly fanavaranAuthService: FanavaranAuthService,
private readonly fanavaranLookupService: FanavaranLookupService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly fileMakerDbService: FileMakerDbService,
private readonly fileReviewerDbService: FileReviewerDbService,
private readonly fieldExpertDbService: FieldExpertDbService,
@@ -7395,13 +7393,7 @@ export class ClaimRequestManagementService {
this.logger.log(
`[executeFanavaranExpertiseSubmit] Already have expertiseId=${claimCase.expertiseId}; skipping Fanavaran POST`,
);
// Catch-up / deduped: SMS may still be pending if expertise predated this notify.
await this.notifyClaimOwnerFanavaranClaimRegistered({
claimCaseId,
claimId: claimCase.claimId,
claimNo: claimCase.claimNo,
logPrefix: `[Fanavaran ${clientKey} V2 Expertise]`,
});
// Product decision: completing the final Fanavaran stage must not send SMS.
return (
(claimCase as any)?.fanavaranSync?.expertise?.response ?? {
Id: claimCase.expertiseId,
@@ -7477,13 +7469,7 @@ export class ClaimRequestManagementService {
},
});
// Best-effort: SMS after last Fanavaran stage (expertise) succeeds
await this.notifyClaimOwnerFanavaranClaimRegistered({
claimCaseId,
claimId: claimCase.claimId,
claimNo: claimCase.claimNo,
logPrefix: `[Fanavaran ${clientKey} V2 Expertise]`,
});
// Product decision: completing the final Fanavaran stage must not send SMS.
return response.data;
} catch (error) {
@@ -7521,12 +7507,7 @@ export class ClaimRequestManagementService {
};
}
if (claimCase.expertiseId != null) {
await this.notifyClaimOwnerFanavaranClaimRegistered({
claimCaseId,
claimId: claimCase.claimId,
claimNo: claimCase.claimNo,
logPrefix,
});
// Product decision: an already-completed Fanavaran stage must not send SMS.
return {
attempted: false,
submitted: false,
@@ -8695,111 +8676,6 @@ export class ClaimRequestManagementService {
return true;
}
/**
* Phone for the claim owner / damaged party (same recipient as other claim SMS).
*/
private async resolveFanavaranClaimOwnerPhone(
claimCase: any,
): Promise<string | undefined> {
const notifyUserId =
claimCase?.damagedPartyUserId ?? claimCase?.owner?.userId;
if (!notifyUserId) return undefined;
const ownerUserId = String(notifyUserId);
if (claimCase.blameRequestId) {
const blame = await this.blameRequestDbService.findById(
String(claimCase.blameRequestId),
);
const ownerParty = (blame?.parties || []).find(
(p: any) =>
p?.person?.userId && String(p.person.userId) === ownerUserId,
);
const fromParty = ownerParty?.person?.phoneNumber;
if (typeof fromParty === "string" && fromParty.trim()) {
return fromParty.trim();
}
}
const user = await this.userDbService.findOne({
_id: new Types.ObjectId(ownerUserId),
});
const mobile = user?.mobile;
if (typeof mobile === "string" && mobile.trim()) return mobile.trim();
return undefined;
}
/**
* After Fanavaran expertise (last stage) succeeds, SMS the claim owner with
* publicId + claimId + ClaimNo. Uses SmsOrchestrationService → same provider
* as login for this deployment (`SMS` / `SMS_PROVIDER`). Never throws.
*/
private async notifyClaimOwnerFanavaranClaimRegistered(input: {
claimCaseId: string;
claimId?: number | string | null;
claimNo?: number | string | null;
logPrefix: string;
}): Promise<void> {
try {
if (input.claimId == null && input.claimNo == null) {
this.logger.warn(
`${input.logPrefix} Skip Fanavaran SMS: no claimId/ClaimNo on response`,
);
return;
}
const claimCase = await this.claimCaseDbService.findById(
input.claimCaseId,
);
if (!claimCase) return;
const sync = (claimCase as any)?.fanavaranSync;
// Dedup: expertise stage, or older base-claim notify from before the move.
if (sync?.expertise?.smsNotifiedAt || sync?.baseClaim?.smsNotifiedAt) {
this.logger.log(
`${input.logPrefix} Fanavaran SMS already sent; skipping duplicate`,
);
return;
}
const phone = await this.resolveFanavaranClaimOwnerPhone(claimCase);
if (!phone) {
this.logger.warn(
`${input.logPrefix} Skip Fanavaran SMS: no phone for claim owner`,
);
return;
}
const publicId =
typeof claimCase.publicId === "string" && claimCase.publicId.trim()
? claimCase.publicId.trim()
: String(input.claimCaseId);
const sent =
await this.smsOrchestrationService.sendFanavaranClaimRegisteredNotice({
receptor: phone,
publicId,
claimNo: input.claimNo ?? claimCase.claimNo,
claimId: input.claimId ?? claimCase.claimId,
});
if (sent) {
await this.claimCaseDbService.findByIdAndUpdate(input.claimCaseId, {
$set: {
"fanavaranSync.expertise.smsNotifiedAt": new Date(),
},
});
this.logger.log(
`${input.logPrefix} Fanavaran claim SMS sent to claim owner phone=${phone} publicId=${publicId} claimNo=${input.claimNo ?? claimCase.claimNo ?? "-"} claimId=${input.claimId ?? claimCase.claimId ?? "-"}`,
);
}
} catch (error) {
this.logger.error(
`${input.logPrefix} Fanavaran claim SMS failed (non-fatal)`,
error,
);
}
}
private async executeFanavaranV2Submit(
claimCaseId: string,
clientKey: FanavaranClientKey,

View File

@@ -142,7 +142,7 @@ export class FanavaranSyncStage {
@Prop({ type: Date })
lastPayloadBuiltAt?: Date;
/** When we SMS'd the claim owner about Fanavaran claimId/ClaimNo (after expertise). */
/** Legacy marker from the retired post-expertise Fanavaran SMS flow. */
@Prop({ type: Date })
smsNotifiedAt?: Date;

View File

@@ -52,7 +52,7 @@ export class ExternalInquiryFlagsDto implements ExternalInquiryFlags {
@ApiProperty({
description:
"ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`). Required for the VIN initial-form path.",
"ESG two-factor VIN/chassis inquiry (`/inquiry/carByChassis`). Required for the VIN initial-form path.",
example: false,
})
@IsBoolean()

View File

@@ -1,4 +1,5 @@
import { ExpertInsurerService } from "./expert-insurer.service";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
describe("insurer expert file summaries", () => {
const service = Object.create(
@@ -49,4 +50,25 @@ describe("insurer expert file summaries", () => {
expect(summary.reviewDurationMinutes).toBe(0);
});
it("exposes the four Fanavaran claim references in insurer claim detail", async () => {
const detail = await (service as any).buildInsurerClaimDetail(
{
_id: "6aa50ce70545fc1906433c86",
status: ClaimCaseStatus.COMPLETED,
claimId: 101,
claimNo: 102,
dmgCaseId: 103,
expertiseId: 104,
},
null,
);
expect(detail.fanavaran).toEqual({
claimId: 101,
claimNo: 102,
dmgCaseId: 103,
expertiseId: 104,
});
});
});

View File

@@ -86,6 +86,7 @@ import {
} from "./helper/timeline-fa-labels";
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
import { serializeDamagedPartSelectionHistory } from "src/helpers/claim-damaged-part-audit";
import { fanavaranClaimReferences } from "src/claim-request-management/fanavaran-claim-references";
@Injectable()
export class ExpertInsurerService {
@@ -786,6 +787,7 @@ export class ExpertInsurerService {
publicId: claim.publicId,
status: claim.status,
claimStatus: claim.claimStatus,
fanavaran: fanavaranClaimReferences(claim),
blameDocumentResendPending: claim.blameDocumentResendPending,
workflow: claim.workflow,
owner: ownerOut,

View File

@@ -114,7 +114,7 @@ export class CallCenterBlameV6Controller {
description:
"VIN alternative to `run-inquiry`. " +
"The agent supplies the chassis number and personal data collected from the caller. " +
"ESG chassis lookup (`policyByChassis`) is executed and the result is stored on the " +
"ESG two-factor chassis lookup (`carByChassis`) is executed and the result is stored on the " +
"blame document under `vehicle.vin` (plateId is left empty). " +
"Identical eligibility guards and insurer-company validation as the plate variant. " +
"After this call, proceed to `send-link` exactly as in the plate flow.",

View File

@@ -1,4 +1,5 @@
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "./entities/schema/partyRole.enum";
import { RequestManagementService } from "./request-management.service";
describe("RequestManagementService policyholder inquiry routing", () => {
@@ -138,7 +139,7 @@ describe("RequestManagementService policyholder inquiry routing", () => {
it("queries VIN with the third-party policyholder", async () => {
const service = Object.create(RequestManagementService.prototype) as any;
service.sandHubService = {
getPolicyByChassisInquiry: jest.fn().mockResolvedValue({
getCarByChassisInquiry: jest.fn().mockResolvedValue({
raw: {},
mapped: { CompanyName: "پارسیان" },
}),
@@ -151,10 +152,10 @@ describe("RequestManagementService policyholder inquiry routing", () => {
await service.getThirdPartyVinInquiry(submission);
expect(
service.sandHubService.getPolicyByChassisInquiry,
service.sandHubService.getCarByChassisInquiry,
).toHaveBeenCalledTimes(1);
expect(
service.sandHubService.getPolicyByChassisInquiry,
service.sandHubService.getCarByChassisInquiry,
).toHaveBeenCalledWith(
{
nationalCode: "0022222222",
@@ -163,4 +164,62 @@ describe("RequestManagementService policyholder inquiry routing", () => {
undefined,
);
});
it("runs both third-party and car-body inquiries for a car-body VIN", async () => {
const service = Object.create(RequestManagementService.prototype) as any;
service.getThirdPartyVinInquiry = jest.fn().mockResolvedValue({
raw: { success: true },
mapped: {
CompanyCode: "8",
CompanyName: "بیمه پارسیان",
PrntPlcyCmpDocNo: "TP-1",
},
});
service.sandHubService = {
getCarBodyInquiry: jest.fn().mockResolvedValue({
source: "ESG_CAR_BODY_VIN_INQUIRY",
raw: { policyId: 123 },
mapped: {
policyNumber: "BODY-1",
companyId: "8",
CompanyName: "بیمه پارسیان",
VinNumberField: vehicle.vin,
},
}),
};
service.clientService = {
findOrCreateClientByCompanyCode: jest
.fn()
.mockResolvedValue({ _id: "client-third-party" }),
};
service.resolveCarBodyPolicyClientId = jest
.fn()
.mockResolvedValue("client-car-body");
service.runParticipantPersonalInquiries = jest.fn().mockResolvedValue(undefined);
service.runVehicleOwnershipInquiry = jest.fn().mockResolvedValue(undefined);
const req: any = {
_id: "car-body-vin-request",
type: BlameRequestType.CAR_BODY,
inquiries: {},
};
const party: any = { person: {}, vehicle: {}, insurance: {} };
await service.runPartyInquiriesVinV3Internal(
req,
participantInput(BlameRequestType.CAR_BODY),
PartyRole.FIRST,
party,
);
expect(service.getThirdPartyVinInquiry).toHaveBeenCalledTimes(1);
expect(service.sandHubService.getCarBodyInquiry).toHaveBeenCalledWith({
nationalCodeOfInsurer: "0033333333",
plate: vehicle.vin,
});
expect(req.inquiries.thirdParty.has).toBe(true);
expect(req.inquiries.carBody.has).toBe(true);
expect(party.insurance.policyNumber).toBe("TP-1");
expect(party.insurance.carBodyInsurance.policyNumber).toBe("BODY-1");
});
});

View File

@@ -465,7 +465,7 @@ export class RequestManagementService {
);
}
const subjects = resolveInquirySubjects(submission);
const result = await this.sandHubService.getPolicyByChassisInquiry(
const result = await this.sandHubService.getCarByChassisInquiry(
{
nationalCode: subjects.thirdPartyPolicyNationalCode,
chassis,
@@ -2142,7 +2142,7 @@ export class RequestManagementService {
* V2 initial-form submitted with a VIN/chassis number instead of a plate.
*
* Performs:
* 1. ESG `/inquiry/policyByChassis` (or its mock when `vinChassis` is off)
* 1. ESG two-factor `/inquiry/carByChassis` (or its mock when `vinChassis` is off)
* 2. Personal identity inquiry (same as the plate path)
*
* The inquiry result is mapped through the same `mapEsgPolicyByPlateToOldFormat`
@@ -2236,7 +2236,7 @@ export class RequestManagementService {
}
}
// ---- External inquiry: ESG policyByChassis ----
// ---- External inquiry: ESG two-factor carByChassis ----
const inquiryClientId = party.person?.clientId
? String(party.person.clientId)
: undefined;
@@ -2256,10 +2256,10 @@ export class RequestManagementService {
inquiryRaw = inquiry.raw;
inquiryMapped = inquiry.mapped;
this.logger.log(
`[ESG] policyByChassis raw for request=${req._id}: ${JSON.stringify(inquiryRaw)}`,
`[ESG] carByChassis raw for request=${req._id}: ${JSON.stringify(inquiryRaw)}`,
);
this.logger.log(
`[ESG] policyByChassis mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
`[ESG] carByChassis mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
);
this.recordPartyCaseInquiryStatus(
req,
@@ -2274,11 +2274,11 @@ export class RequestManagementService {
);
} catch (err: any) {
this.logger.error(
`[ESG] policyByChassis failed for request=${req._id}: ${err?.message || err}`,
`[ESG] carByChassis failed for request=${req._id}: ${err?.message || err}`,
);
if (err?.response) {
this.logger.error(
`[ESG] policyByChassis response for request=${req._id}: status=${
`[ESG] carByChassis response for request=${req._id}: status=${
err.response.status
}, data=${JSON.stringify(err.response.data)}`,
);
@@ -2300,7 +2300,7 @@ export class RequestManagementService {
if (inquiryMapped?.Error) {
this.logger.warn(
`[ESG] policyByChassis error for request=${req._id}: ${JSON.stringify(inquiryMapped.Error)}`,
`[ESG] carByChassis error for request=${req._id}: ${JSON.stringify(inquiryMapped.Error)}`,
);
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, false, {
source: "ESG_VIN_INQUIRY",
@@ -10645,7 +10645,7 @@ export class RequestManagementService {
/**
* VIN variant of `runInquiriesV3`.
* Identical flow, but calls `getPolicyByChassisInquiry` instead of
* Identical flow, but calls the two-factor `getCarByChassisInquiry` instead of
* `getTejaratBlockInquiry` for the primary policy lookup.
*/
async runInquiriesVinV3(
@@ -10788,7 +10788,7 @@ export class RequestManagementService {
/**
* VIN variant of `runPartyInquiriesV3Internal`.
* Calls `getPolicyByChassisInquiry` (ESG chassis lookup) instead of
* Calls `getCarByChassisInquiry` (ESG two-factor chassis lookup) instead of
* `getTejaratBlockInquiry` (plate-based). All other inquiries (personal,
* driving licence, car-body for CAR_BODY) are unchanged.
*/

View File

@@ -189,7 +189,7 @@ describe("SandHubService inquiry mocks", () => {
},
});
const result = await service.getPolicyByChassisInquiry({
const result = await service.getCarByChassisInquiry({
nationalCode: "0012345678",
chassis: "NAAR03HFFRDE07024",
});
@@ -239,7 +239,7 @@ describe("SandHubService inquiry mocks", () => {
});
await expect(
service.getPolicyByChassisInquiry(
service.getCarByChassisInquiry(
{
nationalCode: "1234567890",
chassis: "NAAR03HFFRDE07024",
@@ -256,13 +256,13 @@ describe("SandHubService inquiry mocks", () => {
data: { CmpCod: "8", CmpNam: "بیمه پارسیان" },
});
await service.getPolicyByChassisInquiry({
await service.getCarByChassisInquiry({
nationalCode: "0012345678",
chassis: "NAAR03HFFRDE07024",
});
expect(esg).toHaveBeenCalledWith(
expect.stringContaining("/inquiry/policyByChassis"),
expect.stringContaining("/inquiry/carByChassis"),
{
nationalCode: "0012345678",
chassis: "NAAR03HFFRDE07024",

View File

@@ -1187,7 +1187,7 @@ export class SandHubService {
}
/**
* ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`).
* ESG two-factor VIN/chassis inquiry (`/inquiry/carByChassis`).
*
* When `vinChassis` inquiry is disabled (mock mode) the response shape mirrors
* `buildMockPlateInquiryRaw` so the downstream mapper (`mapEsgPolicyByPlateToOldFormat`)
@@ -1196,12 +1196,12 @@ export class SandHubService {
* @param identity - policyholder national code and 17-character VIN/chassis
* @param options - optional per-tenant client scope
*/
async getPolicyByChassisInquiry(
async getCarByChassisInquiry(
identity: { nationalCode: string; chassis: string },
options?: SandHubInquiryOptions,
): Promise<{ raw: any; mapped: any }> {
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
const requestUrl = `${baseUrl}/inquiry/policyByChassis`;
const requestUrl = `${baseUrl}/inquiry/carByChassis`;
const requestPayload = {
nationalCode: String(identity.nationalCode),
chassis: String(identity.chassis),
@@ -1213,7 +1213,7 @@ export class SandHubService {
const ctx = await this.mockCompanyContext(options);
const raw = this.buildMockPlateInquiryRaw(ctx);
this.logger.debug(
`[MOCK] getPolicyByChassisInquiry nationalCode=${identity.nationalCode} chassis=${identity.chassis}`,
`[MOCK] getCarByChassisInquiry nationalCode=${identity.nationalCode} chassis=${identity.chassis}`,
);
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw, "thirdPartyVin");
if (!mapped?.Error) {