diff --git a/src/core/config/fanavaran-client.config.ts b/src/core/config/fanavaran-client.config.ts new file mode 100644 index 0000000..0777f68 --- /dev/null +++ b/src/core/config/fanavaran-client.config.ts @@ -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}`; +} diff --git a/src/request-management/dto/reinquiry-inquiries.dto.ts b/src/request-management/dto/reinquiry-inquiries.dto.ts new file mode 100644 index 0000000..ee4b410 --- /dev/null +++ b/src/request-management/dto/reinquiry-inquiries.dto.ts @@ -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; + /** What would be written to inquiries.person.data[role] */ + person?: Record; + /** 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; + person?: Record; + }; + parties: ReinquiryPartyResultDto[]; +} + +export class ReinquiryInquiriesResponseDto { + dryRun: boolean; + docsSeen: number; + blameDocsUpdated: number; + claimDocsUpdated: number; + results: ReinquiryCaseResultDto[]; +} diff --git a/src/request-management/inquiry-refresh.controller.ts b/src/request-management/inquiry-refresh.controller.ts new file mode 100644 index 0000000..a00e1a9 --- /dev/null +++ b/src/request-management/inquiry-refresh.controller.ts @@ -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 { + 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", + ); + } + } +} diff --git a/src/request-management/inquiry-refresh.service.ts b/src/request-management/inquiry-refresh.service.ts new file mode 100644 index 0000000..f78ca3b --- /dev/null +++ b/src/request-management/inquiry-refresh.service.ts @@ -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 { + 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 = { + 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, + roles, + dryRun, + ); + summary.results.push(caseResult); + if (caseResult.blameUpdated) summary.blameDocsUpdated += 1; + summary.claimDocsUpdated += caseResult.claimsUpdated; + } + + return summary; + } + + private async refreshBlameCase( + doc: Record, + roles: PartyRole[], + dryRun: boolean, + ): Promise { + 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, + party: Record, + role: PartyRole, + inquiries: Record, + dryRun: boolean, + caseLabel: string, + ): Promise<{ + party: Record; + 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, + raw: Record, + mapped: Record, + ): Record { + 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, + ): Promise { + const claims = await this.claimCaseDbService.find({ + blameRequestId: new Types.ObjectId(blameRequestId), + }); + + const inquiryPatch: Record = {}; + 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, + key: "thirdParty" | "person", + role: PartyRole, + has: boolean, + data: Record = {}, + 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 { + return { + message: error?.message || String(error), + status: error?.status ?? error?.response?.status, + data: error?.data ?? error?.response?.data, + }; + } + + private resolvePartyPlate(party: Record): 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 { + 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 { + 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 !== ""); + } +} diff --git a/src/request-management/request-management.module.ts b/src/request-management/request-management.module.ts index c2b4f08..c5d84e1 100644 --- a/src/request-management/request-management.module.ts +++ b/src/request-management/request-management.module.ts @@ -42,6 +42,8 @@ import { ExpertInitiatedV2Controller } from "./expert-initiated.v2.controller"; import { ExpertInitiatedBlameMirrorController } from "./expert-initiated-blame.mirror.controller"; import { RegistrarInitiatedController } from "./registrar-initiated.controller"; import { RegistrarBlameMirrorController } from "./registrar-blame.mirror.controller"; +import { InquiryRefreshController } from "./inquiry-refresh.controller"; +import { InquiryRefreshService } from "./inquiry-refresh.service"; @Module({ imports: [ @@ -77,9 +79,11 @@ import { RegistrarBlameMirrorController } from "./registrar-blame.mirror.control ExpertInitiatedBlameMirrorController, RegistrarInitiatedController, RegistrarBlameMirrorController, + InquiryRefreshController, ], providers: [ RequestManagementService, + InquiryRefreshService, BlameVideoDbService, BlameVoiceDbService, UserSignDbService,