forked from Yara724/api
inquiry refresh service + fanavaran client config
This commit is contained in:
98
src/core/config/fanavaran-client.config.ts
Normal file
98
src/core/config/fanavaran-client.config.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
export type FanavaranClientKey = "parsian" | "tejaratno";
|
||||||
|
|
||||||
|
export interface FanavaranAuthConfig {
|
||||||
|
appName: string;
|
||||||
|
secret: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
corpId: string;
|
||||||
|
contractId: string;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FanavaranPayloadDefaults {
|
||||||
|
AccidentCityId: number;
|
||||||
|
AccidentReportTypeId: number;
|
||||||
|
AccidentVehicleUsedId: number;
|
||||||
|
ClaimExpertId: number;
|
||||||
|
CompensationReferenceId: number;
|
||||||
|
CulpritLicenceTypeId: number;
|
||||||
|
CulpritTypeId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FanavaranClientProfile {
|
||||||
|
key: FanavaranClientKey;
|
||||||
|
auth: FanavaranAuthConfig;
|
||||||
|
defaults: FanavaranPayloadDefaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FANAVARAN_CLIENT_PROFILES: Record<
|
||||||
|
FanavaranClientKey,
|
||||||
|
FanavaranClientProfile
|
||||||
|
> = {
|
||||||
|
tejaratno: {
|
||||||
|
key: "tejaratno",
|
||||||
|
auth: {
|
||||||
|
appName: "fanhab",
|
||||||
|
secret: "5Fa@N#A2B",
|
||||||
|
username: "fanhabUser",
|
||||||
|
password: "Fan#@2U$3er",
|
||||||
|
corpId: "3539",
|
||||||
|
contractId: "263",
|
||||||
|
location: "100",
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
AccidentCityId: 701,
|
||||||
|
AccidentReportTypeId: 155,
|
||||||
|
AccidentVehicleUsedId: 1,
|
||||||
|
ClaimExpertId: 1589,
|
||||||
|
CompensationReferenceId: 167,
|
||||||
|
CulpritLicenceTypeId: 2,
|
||||||
|
CulpritTypeId: 337,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parsian: {
|
||||||
|
key: "parsian",
|
||||||
|
auth: {
|
||||||
|
appName: "ParsianService",
|
||||||
|
secret: "P@r30@n$erv!ce",
|
||||||
|
username: "ParsianServiceUser",
|
||||||
|
password: "P@r30@n123",
|
||||||
|
corpId: "543",
|
||||||
|
contractId: "28",
|
||||||
|
location: "210050",
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
AccidentCityId: 701,
|
||||||
|
AccidentReportTypeId: 155,
|
||||||
|
AccidentVehicleUsedId: 1,
|
||||||
|
ClaimExpertId: 154,
|
||||||
|
CompensationReferenceId: 167,
|
||||||
|
CulpritLicenceTypeId: 2,
|
||||||
|
CulpritTypeId: 337,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Resolve active Fanavaran tenant from env (`FANAVARAN_CLIENT`) with optional CLIENT_ID fallback. */
|
||||||
|
export function resolveFanavaranClientKey(): FanavaranClientKey {
|
||||||
|
const explicit = process.env.FANAVARAN_CLIENT?.trim().toLowerCase();
|
||||||
|
if (explicit === "parsian" || explicit === "tejaratno") {
|
||||||
|
return explicit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional deployment hint when FANAVARAN_CLIENT is not set.
|
||||||
|
if (String(process.env.CLIENT_ID ?? "") === "8") {
|
||||||
|
return "parsian";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "tejaratno";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFanavaranClientProfile(): FanavaranClientProfile {
|
||||||
|
return FANAVARAN_CLIENT_PROFILES[resolveFanavaranClientKey()];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fanavaranManualSubmitPath(claimCaseId: string): string {
|
||||||
|
return `/v2/claim-request-management/fanavaran-submit/${claimCaseId}`;
|
||||||
|
}
|
||||||
118
src/request-management/dto/reinquiry-inquiries.dto.ts
Normal file
118
src/request-management/dto/reinquiry-inquiries.dto.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Min,
|
||||||
|
} from "class-validator";
|
||||||
|
import { PartyRole } from "../entities/schema/partyRole.enum";
|
||||||
|
|
||||||
|
export class ReinquiryInquiriesDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Blame public id (e.g. A00041). Use for single-case refresh.",
|
||||||
|
example: "A00041",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
publicId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Blame case Mongo id. Alternative to publicId.",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
blameRequestId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Bulk mode: max number of blame cases to process (0 = no limit).",
|
||||||
|
default: 0,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
limit?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Party roles to refresh. Defaults to FIRST and SECOND.",
|
||||||
|
enum: PartyRole,
|
||||||
|
isArray: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsEnum(PartyRole, { each: true })
|
||||||
|
roles?: PartyRole[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "When true, runs inquiries but does not persist updates.",
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
dryRun?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReinquiryPartyInputDto {
|
||||||
|
plateId?: string;
|
||||||
|
plate?: {
|
||||||
|
ir: number;
|
||||||
|
leftDigits: number;
|
||||||
|
centerAlphabet: string;
|
||||||
|
centerDigits: number;
|
||||||
|
};
|
||||||
|
nationalCode?: string;
|
||||||
|
birthDate?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReinquiryPartyPreviewDto {
|
||||||
|
/** What would be written to inquiries.thirdParty.data[role] */
|
||||||
|
thirdParty?: Record<string, unknown>;
|
||||||
|
/** What would be written to inquiries.person.data[role] */
|
||||||
|
person?: Record<string, unknown>;
|
||||||
|
/** Summary of parties[].vehicle / parties[].insurance updates */
|
||||||
|
party?: {
|
||||||
|
vehicle?: { name?: string; type?: string; plateId?: string };
|
||||||
|
insurance?: {
|
||||||
|
policyNumber?: string;
|
||||||
|
company?: string;
|
||||||
|
financialCeiling?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReinquiryPartyResultDto {
|
||||||
|
role: PartyRole;
|
||||||
|
/** Values read from blameCase.parties[role] before calling external APIs */
|
||||||
|
input?: ReinquiryPartyInputDto;
|
||||||
|
/** Present when dryRun=true — shows what would be saved (not written to DB) */
|
||||||
|
preview?: ReinquiryPartyPreviewDto;
|
||||||
|
thirdParty?: { ok: boolean; message?: string };
|
||||||
|
person?: { ok: boolean; message?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReinquiryCaseResultDto {
|
||||||
|
blameRequestId: string;
|
||||||
|
publicId?: string;
|
||||||
|
blameUpdated: boolean;
|
||||||
|
claimsUpdated: number;
|
||||||
|
/** Full inquiries object that would be saved (dryRun only) */
|
||||||
|
inquiriesPreview?: {
|
||||||
|
thirdParty?: Record<string, unknown>;
|
||||||
|
person?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
parties: ReinquiryPartyResultDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReinquiryInquiriesResponseDto {
|
||||||
|
dryRun: boolean;
|
||||||
|
docsSeen: number;
|
||||||
|
blameDocsUpdated: number;
|
||||||
|
claimDocsUpdated: number;
|
||||||
|
results: ReinquiryCaseResultDto[];
|
||||||
|
}
|
||||||
56
src/request-management/inquiry-refresh.controller.ts
Normal file
56
src/request-management/inquiry-refresh.controller.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpException,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import {
|
||||||
|
ReinquiryInquiriesDto,
|
||||||
|
ReinquiryInquiriesResponseDto,
|
||||||
|
} from "./dto/reinquiry-inquiries.dto";
|
||||||
|
import { InquiryRefreshService } from "./inquiry-refresh.service";
|
||||||
|
|
||||||
|
@ApiTags("blame-inquiry-refresh")
|
||||||
|
@Controller("v2/blame-inquiries")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.ADMIN, RoleEnum.FIELD_EXPERT)
|
||||||
|
export class InquiryRefreshController {
|
||||||
|
constructor(private readonly inquiryRefreshService: InquiryRefreshService) {}
|
||||||
|
|
||||||
|
@Post("reinquiry")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Re-run third-party and person inquiries for blame/claim cases",
|
||||||
|
description:
|
||||||
|
"Refreshes inquiries for FIRST/SECOND parties on a blame case, updates parties + inquiries on blameCases, " +
|
||||||
|
"and syncs inquiries.thirdParty + inquiries.person to linked claimCases. " +
|
||||||
|
"Use publicId or blameRequestId for one case, or limit for bulk.",
|
||||||
|
})
|
||||||
|
@ApiBody({ type: ReinquiryInquiriesDto })
|
||||||
|
@ApiResponse({ status: 200, type: ReinquiryInquiriesResponseDto })
|
||||||
|
async reinquiry(
|
||||||
|
@Body() body: ReinquiryInquiriesDto,
|
||||||
|
): Promise<ReinquiryInquiriesResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.inquiryRefreshService.reinquiryInquiries(body);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to refresh inquiries",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
546
src/request-management/inquiry-refresh.service.ts
Normal file
546
src/request-management/inquiry-refresh.service.ts
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
NotFoundException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
|
||||||
|
import { SandHubService } from "src/sand-hub/sand-hub.service";
|
||||||
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
|
import { BlameRequestDbService } from "./entities/db-service/blame-request.db.service";
|
||||||
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
|
import {
|
||||||
|
ReinquiryCaseResultDto,
|
||||||
|
ReinquiryInquiriesDto,
|
||||||
|
ReinquiryInquiriesResponseDto,
|
||||||
|
ReinquiryPartyResultDto,
|
||||||
|
} from "./dto/reinquiry-inquiries.dto";
|
||||||
|
|
||||||
|
type PlateParts = {
|
||||||
|
leftDigits: number;
|
||||||
|
centerAlphabet: string;
|
||||||
|
centerDigits: number;
|
||||||
|
ir: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class InquiryRefreshService {
|
||||||
|
private readonly logger = new Logger(InquiryRefreshService.name);
|
||||||
|
private lastRequestAt = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly blameRequestDbService: BlameRequestDbService,
|
||||||
|
private readonly claimCaseDbService: ClaimCaseDbService,
|
||||||
|
private readonly sandHubService: SandHubService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async reinquiryInquiries(
|
||||||
|
body: ReinquiryInquiriesDto,
|
||||||
|
): Promise<ReinquiryInquiriesResponseDto> {
|
||||||
|
const dryRun = body.dryRun === true;
|
||||||
|
const roles = body.roles?.length
|
||||||
|
? body.roles
|
||||||
|
: [PartyRole.FIRST, PartyRole.SECOND];
|
||||||
|
const limit = body.limit && body.limit > 0 ? body.limit : 0;
|
||||||
|
|
||||||
|
if (!body.publicId && !body.blameRequestId && limit === 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Provide publicId, blameRequestId, or limit for bulk refresh.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter: Record<string, unknown> = {
|
||||||
|
type: { $in: [BlameRequestType.THIRD_PARTY, BlameRequestType.CAR_BODY] },
|
||||||
|
};
|
||||||
|
if (body.publicId) filter.publicId = body.publicId;
|
||||||
|
if (body.blameRequestId) {
|
||||||
|
if (!Types.ObjectId.isValid(body.blameRequestId)) {
|
||||||
|
throw new BadRequestException("Invalid blameRequestId");
|
||||||
|
}
|
||||||
|
filter._id = new Types.ObjectId(body.blameRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let docs = await this.blameRequestDbService.find(filter, { lean: true });
|
||||||
|
if (!docs.length) {
|
||||||
|
throw new NotFoundException("No matching blame cases found");
|
||||||
|
}
|
||||||
|
docs = limit > 0 ? docs.slice(0, limit) : docs;
|
||||||
|
|
||||||
|
const summary: ReinquiryInquiriesResponseDto = {
|
||||||
|
dryRun,
|
||||||
|
docsSeen: docs.length,
|
||||||
|
blameDocsUpdated: 0,
|
||||||
|
claimDocsUpdated: 0,
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
const caseResult = await this.refreshBlameCase(
|
||||||
|
doc as Record<string, any>,
|
||||||
|
roles,
|
||||||
|
dryRun,
|
||||||
|
);
|
||||||
|
summary.results.push(caseResult);
|
||||||
|
if (caseResult.blameUpdated) summary.blameDocsUpdated += 1;
|
||||||
|
summary.claimDocsUpdated += caseResult.claimsUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshBlameCase(
|
||||||
|
doc: Record<string, any>,
|
||||||
|
roles: PartyRole[],
|
||||||
|
dryRun: boolean,
|
||||||
|
): Promise<ReinquiryCaseResultDto> {
|
||||||
|
const label = doc.publicId || doc.requestNo || String(doc._id);
|
||||||
|
const parties = Array.isArray(doc.parties) ? [...doc.parties] : [];
|
||||||
|
const inquiries =
|
||||||
|
doc.inquiries && typeof doc.inquiries === "object"
|
||||||
|
? { ...doc.inquiries }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
let blameChanged = false;
|
||||||
|
const partyResults: ReinquiryPartyResultDto[] = [];
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
const index = parties.findIndex((party) => party?.role === role);
|
||||||
|
if (index === -1) {
|
||||||
|
partyResults.push({
|
||||||
|
role,
|
||||||
|
thirdParty: { ok: false, message: "party not found" },
|
||||||
|
person: { ok: false, message: "party not found" },
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const party = parties[index];
|
||||||
|
const partyResult = await this.refreshPartyInquiries(
|
||||||
|
doc,
|
||||||
|
party,
|
||||||
|
role,
|
||||||
|
inquiries,
|
||||||
|
dryRun,
|
||||||
|
label,
|
||||||
|
);
|
||||||
|
partyResults.push(partyResult.result);
|
||||||
|
|
||||||
|
if (partyResult.partyChanged) {
|
||||||
|
parties[index] = partyResult.party;
|
||||||
|
blameChanged = true;
|
||||||
|
}
|
||||||
|
if (partyResult.inquiriesChanged) {
|
||||||
|
blameChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let claimsUpdated = 0;
|
||||||
|
if (blameChanged && !dryRun) {
|
||||||
|
await this.blameRequestDbService.findByIdAndUpdate(doc._id, {
|
||||||
|
$set: {
|
||||||
|
parties,
|
||||||
|
inquiries,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
claimsUpdated = await this.syncInquiriesToClaims(
|
||||||
|
String(doc._id),
|
||||||
|
inquiries,
|
||||||
|
);
|
||||||
|
} else if (blameChanged && dryRun) {
|
||||||
|
const linkedClaims = await this.claimCaseDbService.find({
|
||||||
|
blameRequestId: new Types.ObjectId(String(doc._id)),
|
||||||
|
});
|
||||||
|
claimsUpdated = linkedClaims.length;
|
||||||
|
this.logger.log(`[dry-run] ${label} would update blame + ${claimsUpdated} claim(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blameRequestId: String(doc._id),
|
||||||
|
publicId: doc.publicId,
|
||||||
|
blameUpdated: blameChanged,
|
||||||
|
claimsUpdated,
|
||||||
|
...(dryRun && blameChanged
|
||||||
|
? {
|
||||||
|
inquiriesPreview: {
|
||||||
|
thirdParty: inquiries.thirdParty,
|
||||||
|
person: inquiries.person,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
parties: partyResults,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshPartyInquiries(
|
||||||
|
doc: Record<string, any>,
|
||||||
|
party: Record<string, any>,
|
||||||
|
role: PartyRole,
|
||||||
|
inquiries: Record<string, any>,
|
||||||
|
dryRun: boolean,
|
||||||
|
caseLabel: string,
|
||||||
|
): Promise<{
|
||||||
|
party: Record<string, any>;
|
||||||
|
partyChanged: boolean;
|
||||||
|
inquiriesChanged: boolean;
|
||||||
|
result: ReinquiryPartyResultDto;
|
||||||
|
}> {
|
||||||
|
const result: ReinquiryPartyResultDto = { role };
|
||||||
|
let nextParty = party;
|
||||||
|
let partyChanged = false;
|
||||||
|
let inquiriesChanged = false;
|
||||||
|
|
||||||
|
const plate = this.resolvePartyPlate(party);
|
||||||
|
const nationalCode =
|
||||||
|
this.cleanString(party?.person?.nationalCodeOfInsurer) ||
|
||||||
|
this.cleanString(party?.person?.nationalCodeOfDriver);
|
||||||
|
const birthDate =
|
||||||
|
party?.person?.insurerBirthday ??
|
||||||
|
party?.person?.driverBirthday ??
|
||||||
|
party?.person?.birthday;
|
||||||
|
|
||||||
|
const inquiryClientId = party?.person?.clientId
|
||||||
|
? String(party.person.clientId)
|
||||||
|
: undefined;
|
||||||
|
const inquiryOptions = inquiryClientId
|
||||||
|
? { clientId: inquiryClientId }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
result.input = {
|
||||||
|
plateId: party?.vehicle?.plateId,
|
||||||
|
...(plate ? { plate } : {}),
|
||||||
|
...(nationalCode ? { nationalCode } : {}),
|
||||||
|
...(birthDate !== null && birthDate !== undefined
|
||||||
|
? { birthDate }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
this.logger.log(
|
||||||
|
`[dry-run] ${caseLabel} ${role} input=${JSON.stringify(result.input)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plate || !nationalCode) {
|
||||||
|
result.thirdParty = {
|
||||||
|
ok: false,
|
||||||
|
message: !plate
|
||||||
|
? "plate not found on party"
|
||||||
|
: "nationalCodeOfInsurer/nationalCodeOfDriver missing",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
await this.waitForRateLimit();
|
||||||
|
try {
|
||||||
|
const inquiry = await this.sandHubService.getTejaratBlockInquiry(
|
||||||
|
{
|
||||||
|
plate: {
|
||||||
|
...plate,
|
||||||
|
nationalCode,
|
||||||
|
},
|
||||||
|
nationalCodeOfInsurer: nationalCode,
|
||||||
|
},
|
||||||
|
inquiryOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (inquiry.mapped?.Error) {
|
||||||
|
this.recordPartyInquiry(inquiries, "thirdParty", role, false, {
|
||||||
|
source: "TEJARAT_BLOCK_INQUIRY",
|
||||||
|
raw: inquiry.raw,
|
||||||
|
mapped: inquiry.mapped,
|
||||||
|
});
|
||||||
|
inquiriesChanged = true;
|
||||||
|
result.thirdParty = {
|
||||||
|
ok: false,
|
||||||
|
message: inquiry.mapped.Error.Message || "third-party inquiry error",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
nextParty = this.applyThirdPartyToParty(nextParty, inquiry.raw, inquiry.mapped);
|
||||||
|
partyChanged = true;
|
||||||
|
this.recordPartyInquiry(inquiries, "thirdParty", role, true, {
|
||||||
|
source: "TEJARAT_BLOCK_INQUIRY",
|
||||||
|
raw: inquiry.raw,
|
||||||
|
mapped: inquiry.mapped,
|
||||||
|
refreshedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
inquiriesChanged = true;
|
||||||
|
result.thirdParty = { ok: true };
|
||||||
|
if (dryRun) {
|
||||||
|
result.preview = {
|
||||||
|
...(result.preview || {}),
|
||||||
|
thirdParty: inquiries.thirdParty?.data?.[role],
|
||||||
|
party: {
|
||||||
|
vehicle: {
|
||||||
|
name: nextParty.vehicle?.name,
|
||||||
|
type: nextParty.vehicle?.type,
|
||||||
|
plateId: nextParty.vehicle?.plateId,
|
||||||
|
},
|
||||||
|
insurance: {
|
||||||
|
policyNumber: nextParty.insurance?.policyNumber,
|
||||||
|
company: nextParty.insurance?.company,
|
||||||
|
financialCeiling: nextParty.insurance?.financialCeiling,
|
||||||
|
startDate: nextParty.insurance?.startDate,
|
||||||
|
endDate: nextParty.insurance?.endDate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.recordPartyInquiry(inquiries, "thirdParty", role, false, {}, error);
|
||||||
|
inquiriesChanged = true;
|
||||||
|
result.thirdParty = {
|
||||||
|
ok: false,
|
||||||
|
message: error?.message || String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nationalCode || birthDate === null || birthDate === undefined) {
|
||||||
|
result.person = {
|
||||||
|
ok: false,
|
||||||
|
message: !nationalCode
|
||||||
|
? "nationalCodeOfInsurer/nationalCodeOfDriver missing"
|
||||||
|
: "insurerBirthday/driverBirthday missing",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
await this.waitForRateLimit();
|
||||||
|
try {
|
||||||
|
const personData = await this.sandHubService.getPersonalInquiry(
|
||||||
|
nationalCode,
|
||||||
|
birthDate,
|
||||||
|
inquiryOptions,
|
||||||
|
);
|
||||||
|
this.recordPartyInquiry(inquiries, "person", role, true, personData);
|
||||||
|
inquiriesChanged = true;
|
||||||
|
result.person = { ok: true };
|
||||||
|
if (dryRun) {
|
||||||
|
result.preview = {
|
||||||
|
...(result.preview || {}),
|
||||||
|
person: inquiries.person?.data?.[role],
|
||||||
|
};
|
||||||
|
this.logger.log(
|
||||||
|
`[dry-run] ${caseLabel} ${role} person preview=${JSON.stringify(result.preview.person)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.recordPartyInquiry(inquiries, "person", role, false, {}, error);
|
||||||
|
inquiriesChanged = true;
|
||||||
|
result.person = {
|
||||||
|
ok: false,
|
||||||
|
message: error?.message || String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun && result.preview?.thirdParty) {
|
||||||
|
this.logger.log(
|
||||||
|
`[dry-run] ${caseLabel} ${role} thirdParty mapped.company=${(result.preview.thirdParty as any)?.mapped?.CompanyName ?? "-"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { party: nextParty, partyChanged, inquiriesChanged, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyThirdPartyToParty(
|
||||||
|
party: Record<string, any>,
|
||||||
|
raw: Record<string, any>,
|
||||||
|
mapped: Record<string, any>,
|
||||||
|
): Record<string, any> {
|
||||||
|
const next = { ...party };
|
||||||
|
next.vehicle = { ...(next.vehicle || {}) };
|
||||||
|
next.insurance = { ...(next.insurance || {}) };
|
||||||
|
|
||||||
|
const existingCarBody = next.vehicle.inquiry?.carBody;
|
||||||
|
|
||||||
|
next.vehicle.inquiry = {
|
||||||
|
source: "TEJARAT_BLOCK_INQUIRY",
|
||||||
|
raw,
|
||||||
|
mapped,
|
||||||
|
refreshedAt: new Date().toISOString(),
|
||||||
|
...(existingCarBody ? { carBody: existingCarBody } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
next.vehicle.name =
|
||||||
|
mapped.vehiclePersianName ||
|
||||||
|
mapped.MapTypNam ||
|
||||||
|
next.vehicle.name ||
|
||||||
|
"اطلاعات این گزینه در استعلام موجود نیست";
|
||||||
|
next.vehicle.type =
|
||||||
|
mapped.persianCarType ||
|
||||||
|
mapped.MapUsageName ||
|
||||||
|
next.vehicle.type;
|
||||||
|
next.insurance.policyNumber =
|
||||||
|
mapped.LastCompanyDocumentNumber ||
|
||||||
|
mapped.insuranceNumber ||
|
||||||
|
next.insurance.policyNumber;
|
||||||
|
next.insurance.company =
|
||||||
|
mapped.companyPersianName || mapped.CompanyName || next.insurance.company;
|
||||||
|
next.insurance.financialCeiling =
|
||||||
|
mapped.financeCoverage || mapped.FinancialCvrCptl || next.insurance.financialCeiling;
|
||||||
|
next.insurance.startDate =
|
||||||
|
mapped.persianStartDate || mapped.IssueDate || mapped.SatrtDate || next.insurance.startDate;
|
||||||
|
next.insurance.endDate =
|
||||||
|
mapped.persianEndDate || mapped.EndDate || next.insurance.endDate;
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncInquiriesToClaims(
|
||||||
|
blameRequestId: string,
|
||||||
|
inquiries: Record<string, any>,
|
||||||
|
): Promise<number> {
|
||||||
|
const claims = await this.claimCaseDbService.find({
|
||||||
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const inquiryPatch: Record<string, unknown> = {};
|
||||||
|
if (inquiries.thirdParty) inquiryPatch["inquiries.thirdParty"] = inquiries.thirdParty;
|
||||||
|
if (inquiries.person) inquiryPatch["inquiries.person"] = inquiries.person;
|
||||||
|
if (!Object.keys(inquiryPatch).length) return 0;
|
||||||
|
|
||||||
|
let updated = 0;
|
||||||
|
for (const claim of claims) {
|
||||||
|
const result = await this.claimCaseDbService.findByIdAndUpdate(
|
||||||
|
String(claim._id),
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
...inquiryPatch,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result) updated += 1;
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private recordPartyInquiry(
|
||||||
|
inquiries: Record<string, any>,
|
||||||
|
key: "thirdParty" | "person",
|
||||||
|
role: PartyRole,
|
||||||
|
has: boolean,
|
||||||
|
data: Record<string, unknown> = {},
|
||||||
|
error?: any,
|
||||||
|
): void {
|
||||||
|
const existingBlock = inquiries[key] || {};
|
||||||
|
const existingData =
|
||||||
|
existingBlock.data &&
|
||||||
|
typeof existingBlock.data === "object" &&
|
||||||
|
!Array.isArray(existingBlock.data)
|
||||||
|
? { ...existingBlock.data }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
inquiries[key] = {
|
||||||
|
has: has || Object.keys(existingData).length > 0,
|
||||||
|
data: {
|
||||||
|
...existingData,
|
||||||
|
[role]: data,
|
||||||
|
},
|
||||||
|
...(error ? { error: this.normalizeInquiryError(error) } : {}),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeInquiryError(error: any): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
message: error?.message || String(error),
|
||||||
|
status: error?.status ?? error?.response?.status,
|
||||||
|
data: error?.data ?? error?.response?.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolvePartyPlate(party: Record<string, any>): PlateParts | null {
|
||||||
|
const fromPlateId = this.parsePlateFromCompactString(party?.vehicle?.plateId);
|
||||||
|
if (fromPlateId) return fromPlateId;
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
party?.vehicle?.inquiry?.mapped,
|
||||||
|
party?.vehicle?.inquiry?.raw,
|
||||||
|
party?.vehicle?.inquiry,
|
||||||
|
party?.vehicle,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const leftDigits = this.firstPresent(candidate.Plk1, candidate.platePartOne);
|
||||||
|
const centerAlphabet = this.firstPresent(
|
||||||
|
candidate.plateLetterid,
|
||||||
|
candidate.plateLetterId,
|
||||||
|
candidate.plateLetterTitle,
|
||||||
|
);
|
||||||
|
const centerDigits = this.firstPresent(candidate.Plk3, candidate.platePartThree);
|
||||||
|
const ir = this.firstPresent(candidate.PlkSrl, candidate.plkSrl, candidate.plateSerialNumber);
|
||||||
|
|
||||||
|
if (
|
||||||
|
leftDigits !== undefined &&
|
||||||
|
centerAlphabet !== undefined &&
|
||||||
|
centerDigits !== undefined &&
|
||||||
|
ir !== undefined
|
||||||
|
) {
|
||||||
|
const plateLetter = String(centerAlphabet).trim();
|
||||||
|
const parsed: PlateParts = {
|
||||||
|
leftDigits: Number(leftDigits),
|
||||||
|
centerAlphabet: plateLetter,
|
||||||
|
centerDigits: Number(centerDigits),
|
||||||
|
ir: Number(ir),
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
Number.isFinite(parsed.leftDigits) &&
|
||||||
|
Number.isFinite(parsed.centerDigits) &&
|
||||||
|
Number.isFinite(parsed.ir) &&
|
||||||
|
parsed.centerAlphabet
|
||||||
|
) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePlateFromCompactString(plateId?: string): PlateParts | null {
|
||||||
|
if (!plateId) return null;
|
||||||
|
const parts = String(plateId).split("-");
|
||||||
|
if (parts.length !== 4) return null;
|
||||||
|
|
||||||
|
const [irRaw, leftRaw, alphaRaw, centerRaw] = parts;
|
||||||
|
const ir = Number(irRaw);
|
||||||
|
const leftDigits = Number(leftRaw);
|
||||||
|
const centerDigits = Number(centerRaw);
|
||||||
|
const centerAlphabet = String(alphaRaw || "").trim();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isFinite(ir) ||
|
||||||
|
!Number.isFinite(leftDigits) ||
|
||||||
|
!Number.isFinite(centerDigits) ||
|
||||||
|
!centerAlphabet
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { leftDigits, centerAlphabet, centerDigits, ir };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForRateLimit(): Promise<void> {
|
||||||
|
const perMinute = Number(process.env.RATE_LIMIT_PER_MINUTE || 5);
|
||||||
|
const minDelayMs = Math.ceil(60000 / Math.max(1, perMinute));
|
||||||
|
const elapsed = Date.now() - this.lastRequestAt;
|
||||||
|
if (this.lastRequestAt > 0 && elapsed < minDelayMs) {
|
||||||
|
await this.delay(minDelayMs - elapsed);
|
||||||
|
}
|
||||||
|
this.lastRequestAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private delay(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanString(value: unknown): string {
|
||||||
|
return value === undefined || value === null ? "" : String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private firstPresent(...values: unknown[]): unknown {
|
||||||
|
return values.find((value) => value !== undefined && value !== null && value !== "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ import { ExpertInitiatedV2Controller } from "./expert-initiated.v2.controller";
|
|||||||
import { ExpertInitiatedBlameMirrorController } from "./expert-initiated-blame.mirror.controller";
|
import { ExpertInitiatedBlameMirrorController } from "./expert-initiated-blame.mirror.controller";
|
||||||
import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
||||||
import { RegistrarBlameMirrorController } from "./registrar-blame.mirror.controller";
|
import { RegistrarBlameMirrorController } from "./registrar-blame.mirror.controller";
|
||||||
|
import { InquiryRefreshController } from "./inquiry-refresh.controller";
|
||||||
|
import { InquiryRefreshService } from "./inquiry-refresh.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -77,9 +79,11 @@ import { RegistrarBlameMirrorController } from "./registrar-blame.mirror.control
|
|||||||
ExpertInitiatedBlameMirrorController,
|
ExpertInitiatedBlameMirrorController,
|
||||||
RegistrarInitiatedController,
|
RegistrarInitiatedController,
|
||||||
RegistrarBlameMirrorController,
|
RegistrarBlameMirrorController,
|
||||||
|
InquiryRefreshController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
RequestManagementService,
|
RequestManagementService,
|
||||||
|
InquiryRefreshService,
|
||||||
BlameVideoDbService,
|
BlameVideoDbService,
|
||||||
BlameVoiceDbService,
|
BlameVoiceDbService,
|
||||||
UserSignDbService,
|
UserSignDbService,
|
||||||
|
|||||||
Reference in New Issue
Block a user