diff --git a/package-lock.json b/package-lock.json index eac96be..ca7663e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "class-validator": "^0.15.1", "express": "^5.2.1", "fastest-levenshtein": "^1.0.16", + "form-data": "^4.0.6", "joi": "^18.2.1", "mongoose": "^8.9.2", "pdfkit": "^0.19.1", diff --git a/package.json b/package.json index 091aad0..ea527ee 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "class-validator": "^0.15.1", "express": "^5.2.1", "fastest-levenshtein": "^1.0.16", + "form-data": "^4.0.6", "joi": "^18.2.1", "mongoose": "^8.9.2", "pdfkit": "^0.19.1", diff --git a/src/claim-request-management/claim-request-management.module.ts b/src/claim-request-management/claim-request-management.module.ts index a2692ab..9afeab6 100644 --- a/src/claim-request-management/claim-request-management.module.ts +++ b/src/claim-request-management/claim-request-management.module.ts @@ -54,11 +54,13 @@ import { JwtModule } from "@nestjs/jwt"; import { MediaPolicyModule } from "src/media-policy/media-policy.module"; import { HttpModule } from "@nestjs/axios"; import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module"; +import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module"; @Module({ imports: [ HttpModule, FanavaranAuditModule, + FanavaranLookupModule, PublicIdModule, UsersModule, RequestManagementModule, diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index b85e157..14fcd3f 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -13,7 +13,9 @@ import { import { Types } from "mongoose"; import { isAxiosError } from "axios"; import { unlink } from "node:fs/promises"; -import { existsSync } from "node:fs"; +import { createReadStream, existsSync } from "node:fs"; +import { basename } from "node:path"; +import FormData from "form-data"; import { AiService } from "src/ai/ai.service"; import { buildFileLink, resolveStoredFileUrl } from "src/helpers/urlCreator"; import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service"; @@ -170,6 +172,8 @@ import { resolveFanavaranClientKey, } from "src/core/config/fanavaran-client.config"; import { FanavaranAuditService } from "src/fanavaran/fanavaran-audit.service"; +import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service"; +import { FANAVARAN_REMOTE_LOOKUPS } from "src/fanavaran/fanavaran-lookup.config"; import type { FanavaranAuditSession } from "src/fanavaran/fanavaran-audit.types"; import { FanavaranAuditSource, @@ -189,8 +193,80 @@ export interface FanavaranAutoSubmitResult { fanavaranResponse?: unknown; } +export interface FanavaranDamageCaseSubmitResult { + attempted: boolean; + submitted: boolean; + skipped?: boolean; + skipReason?: string; + warning?: string; + claimId?: number; + dmgCaseId?: number; + fanavaranResponse?: unknown; +} + +export interface FanavaranAttachmentSubmitResult { + attempted: boolean; + submitted: boolean; + skipped?: boolean; + skipReason?: string; + warning?: string; + claimId?: number; + fileId?: number; + fileName?: string; + fanavaranResponse?: unknown; +} + +export interface FanavaranExpertiseSubmitResult { + attempted: boolean; + submitted: boolean; + skipped?: boolean; + skipReason?: string; + warning?: string; + claimId?: number; + dmgCaseId?: number; + expertiseId?: number; + fanavaranResponse?: unknown; +} + +interface FanavaranAttachmentCandidate { + path: string; + fileName: string; + source: string; + content: Record; + alreadySubmitted: boolean; + submittedFileId?: unknown; +} + const FANAVARAN_ACCIDENT_LOCATION_ADDRESS = "استان تهران شهر تهران"; const FANAVARAN_DEFAULT_ACCIDENT_CAUSE_ID = 6; +const FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT = 1000; + +const FANAVARAN_PLATE_LETTER_CODE: Record = { + الف: 1, + ب: 2, + پ: 3, + ج: 4, + د: 5, + س: 6, + ص: 7, + ط: 8, + ع: 9, + ق: 10, + ل: 11, + م: 12, + ن: 13, + و: 14, + ه: 15, + ی: 16, + ک: 17, + ژ: 18, + ت: 19, + ث: 20, + ز: 21, + ش: 22, + ف: 23, + گ: 24, +}; @Injectable() export class ClaimRequestManagementService { @@ -213,7 +289,7 @@ export class ClaimRequestManagementService { // API headers private readonly CORP_ID = "3539"; private readonly CONTRACT_ID = "263"; - private readonly LOCATION = "100"; + private readonly LOCATION = "119"; private readonly PARSIAN_FANAVARAN_CONFIG = { appName: "ParsianService", @@ -271,6 +347,7 @@ export class ClaimRequestManagementService { private readonly sandHubService: SandHubService, private readonly httpService: HttpService, private readonly fanavaranAuditService: FanavaranAuditService, + private readonly fanavaranLookupService: FanavaranLookupService, ) {} private requiredDocumentKeysV2(isCarBody: boolean): string[] { @@ -3332,6 +3409,10 @@ export class ClaimRequestManagementService { }, 0); } + private normalizeFanavaranEstimateAmount(amount: number): number { + return amount > 0 ? amount : FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT; + } + /** Branch ids referenced on expert-priced parts (`daghi` object with `branchId`). */ private collectBranchIdsFromClaimExpertReply(reply: { parts?: unknown[]; @@ -3449,9 +3530,9 @@ export class ClaimRequestManagementService { result.AccidentLocationAddress = FANAVARAN_ACCIDENT_LOCATION_ADDRESS; - result.EstimateAmount = input.damageParts - ? this.calculateEstimateAmount(input.damageParts) - : 0; + result.EstimateAmount = this.normalizeFanavaranEstimateAmount( + input.damageParts ? this.calculateEstimateAmount(input.damageParts) : 0, + ); this.applyFanavaranDefaultFields(result); @@ -3466,6 +3547,125 @@ export class ClaimRequestManagementService { return result; } + private formatFanavaranSelectedPartsDesc(selectedParts: unknown[]): string { + const labels = selectedParts + .map((part) => { + if (typeof part === "string") return part; + if (part && typeof part === "object") { + const p = part as { + label_fa?: unknown; + titleFa?: unknown; + name?: unknown; + catalogKey?: unknown; + }; + return p.label_fa ?? p.titleFa ?? p.name ?? p.catalogKey; + } + return null; + }) + .filter( + (label): label is string => typeof label === "string" && !!label.trim(), + ) + .map((label) => label.trim()); + + return labels.length ? labels.join("/") : "موارد آسیب دیده خودرو"; + } + + private getFanavaranPlateMiddleCode( + centerAlphabet?: string | number | null, + ): number | null { + if (centerAlphabet == null) return null; + const raw = String(centerAlphabet).trim(); + if (!raw) return null; + const asNumber = Number(raw); + if (Number.isFinite(asNumber)) return asNumber; + return FANAVARAN_PLATE_LETTER_CODE[raw] ?? null; + } + + private formatFanavaranPlateNo( + plate: { + leftDigits: number; + centerAlphabet: string; + centerDigits: number; + ir: number; + } | null, + ): string | null { + if (!plate) return null; + return `${plate.centerDigits}${plate.centerAlphabet}${plate.leftDigits}`; + } + + private buildFanavaranDamageCasePayload(input: { + claimCase: any; + blameCase?: any; + selectedParts: unknown[]; + defaults: { + AccidentVehicleUsedId: number; + CulpritLicenceTypeId: number; + DmgCaseTypeId: number; + DmgHistoryStatus: number; + PlaqueKindId: number; + PlaqueSampleId: number; + DriverIsOwner: number; + FaultPercent: number; + }; + }): Record { + const plate = this.resolveOwnershipPlateForClaim( + input.claimCase, + input.blameCase, + ); + const damagedParty = Array.isArray(input.blameCase?.parties) + ? input.blameCase.parties.find( + (party: any) => + String(party?.person?.userId ?? "") === + String(input.claimCase?.owner?.userId ?? ""), + ) + : null; + const person = damagedParty?.person ?? {}; + const vehicle = damagedParty?.vehicle ?? {}; + const insurance = damagedParty?.insurance ?? {}; + const inquiryMapped = vehicle?.inquiry?.mapped ?? {}; + const inquiryRaw = vehicle?.inquiry?.raw ?? {}; + + return { + BeginDate: insurance.startDate ?? null, + BuiltYear: + Number(inquiryMapped.ModelField ?? inquiryMapped.ModelCii) || null, + ChassisNo: inquiryMapped.ChassisNo ?? inquiryRaw.ChassisNo ?? null, + DmgHistoryStatus: input.defaults.DmgHistoryStatus, + Desc: this.formatFanavaranSelectedPartsDesc(input.selectedParts), + DmgCaseTypeId: input.defaults.DmgCaseTypeId, + DriverId: null, + EndDate: insurance.endDate ?? null, + EstimateAmount: FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT, + FaultPercent: input.defaults.FaultPercent, + InsuranceCorpId: null, + DriverIsOwner: person.driverIsInsurer ? 1 : input.defaults.DriverIsOwner, + LicenceCityId: null, + LicenceCountryId: null, + LicenceForeignCityName: null, + LicenceIssuDate: person.driverBirthday ?? "1394/10/13", + LicenceNo: person.driverLicense ?? "1124242", + LicenceTypeId: input.defaults.CulpritLicenceTypeId, + MotorNo: inquiryMapped.MotorNo ?? inquiryRaw.MotorNo ?? null, + OwnerId: null, + PlaqueCityId: null, + PlaqueKindId: plate ? input.defaults.PlaqueKindId : null, + PlaqueLeftNo: plate ? String(plate.leftDigits) : null, + PlaqueMiddleCodeId: this.getFanavaranPlateMiddleCode( + plate?.centerAlphabet, + ), + PlaqueNo: this.formatFanavaranPlateNo(plate), + PlaqueRightNo: plate ? String(plate.centerDigits) : null, + PlaqueSampleId: plate ? input.defaults.PlaqueSampleId : null, + PlaqueSerial: plate ? String(plate.ir) : null, + PolicyNo: insurance.policyNumber ?? null, + PreviousPolicyEndDate: insurance.endDate ?? "", + VehicleKindId: null, + VIN: inquiryMapped.VIN ?? inquiryRaw.VIN ?? null, + AccidentVehicleUsedId: input.defaults.AccidentVehicleUsedId, + PolicyCINumber: null, + }; + } + /** * Fanavaran Submit - Map data from claim-request-management and request-management to third-party-car-financial-claims format */ @@ -3566,9 +3766,11 @@ export class ClaimRequestManagementService { const damageReply = claimRequest.damageExpertReplyFinal || claimRequest.damageExpertReply; if (damageReply?.parts) { - result.EstimateAmount = this.calculateEstimateAmount(damageReply.parts); + result.EstimateAmount = this.normalizeFanavaranEstimateAmount( + this.calculateEstimateAmount(damageReply.parts), + ); } else { - result.EstimateAmount = 0; + result.EstimateAmount = FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT; } // Keep other fields from template with default values @@ -3882,7 +4084,9 @@ export class ClaimRequestManagementService { }), ); - const policyCount = Array.isArray(response.data) ? response.data.length : 0; + const policyCount = Array.isArray(response.data) + ? response.data.length + : 0; this.logger.log( `${logPrefix} Policy inquiry response status=${response.status} count=${policyCount}`, @@ -3931,9 +4135,7 @@ export class ClaimRequestManagementService { durationMs: Date.now() - startedAt, }); } - this.logger.warn( - `${logPrefix} Policy inquiry failed: ${errorMessage}`, - ); + this.logger.warn(`${logPrefix} Policy inquiry failed: ${errorMessage}`); if (error instanceof BadRequestException) { throw error; } @@ -4269,6 +4471,1467 @@ export class ClaimRequestManagementService { return this.submitFanavaranV2(claimCaseId, "parsian"); } + async previewFanavaranDamageCaseV2( + claimCaseId: string, + clientKey: FanavaranClientKey, + ): Promise { + const profile = getFanavaranClientProfile(clientKey); + const claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) { + throw new NotFoundException("Claim case not found"); + } + if (!claimCase.blameRequestId) { + throw new BadRequestException("Blame case not linked to claim case"); + } + const blameCase = await this.blameRequestDbService.findById( + claimCase.blameRequestId, + ); + if (!blameCase) { + throw new NotFoundException("Blame case not found"); + } + if (blameCase.type !== BlameRequestType.THIRD_PARTY) { + throw new BadRequestException( + "Fanavaran damage-case submit only applies to THIRD_PARTY claims", + ); + } + + const payload = this.buildFanavaranDamageCasePayload({ + claimCase, + blameCase, + selectedParts: claimCase.damage?.selectedParts ?? [], + defaults: profile.defaults, + }); + + return { + clientKey, + claimCaseId, + claimId: claimCase.claimId ?? null, + dmgCaseId: claimCase.dmgCaseId ?? null, + submitUrl: claimCase.claimId + ? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/dmg-cases` + : null, + warning: claimCase.claimId + ? undefined + : "Fanavaran base claimId is required before submit.", + payload, + }; + } + + async submitFanavaranDamageCaseV2( + claimCaseId: string, + clientKey: FanavaranClientKey, + ): Promise { + try { + return await this.executeFanavaranDamageCaseSubmit( + claimCaseId, + clientKey, + undefined, + FanavaranAuditSource.SUBMIT, + ); + } catch (error) { + this.logger.error( + `[Fanavaran ${clientKey} V2] Error submitting damage case to Fanavaran`, + error, + ); + throw new BadGatewayException(this.extractFanavaranErrorMessage(error)); + } + } + + private getSubmittedFanavaranAttachmentFileNames( + claimCase: any, + ): Map { + const submitted = new Map(); + const files = claimCase?.fanavaranSync?.attachments?.files; + if (!Array.isArray(files)) return submitted; + + for (const file of files) { + const fileName = (file as { fileName?: unknown })?.fileName; + if (typeof fileName === "string" && fileName.trim()) { + submitted.set(fileName, (file as { fileId?: unknown })?.fileId); + } + } + + return submitted; + } + + private appendFanavaranAttachmentCandidate( + candidates: FanavaranAttachmentCandidate[], + seen: Set, + submittedFiles: Map, + input: { path?: unknown; fileName?: unknown; source: string }, + fileTypeId: number, + ): void { + const path = typeof input.path === "string" ? input.path : ""; + const fileName = + typeof input.fileName === "string" && input.fileName.trim() + ? input.fileName + : path + ? basename(path) + : ""; + if (!path || !fileName || seen.has(fileName)) return; + + seen.add(fileName); + candidates.push({ + path, + fileName, + source: input.source, + content: { + FileName: fileName, + FileTypeId: fileTypeId, + Files: [{ FileName: fileName, FileTypeId: fileTypeId }], + }, + alreadySubmitted: submittedFiles.has(fileName), + submittedFileId: submittedFiles.get(fileName), + }); + } + + private async collectFanavaranAttachmentCandidates( + claimCaseId: string, + clientKey: FanavaranClientKey, + ): Promise<{ + claimCase: any; + candidates: FanavaranAttachmentCandidate[]; + }> { + const profile = getFanavaranClientProfile(clientKey); + const claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) { + throw new NotFoundException("Claim case not found"); + } + + const candidates: FanavaranAttachmentCandidate[] = []; + const seen = new Set(); + const submittedFiles = + this.getSubmittedFanavaranAttachmentFileNames(claimCase); + const fileTypeId = profile.defaults.ClaimFileTypeId; + + const documents = + await this.claimRequiredDocumentDbService.findByClaimId(claimCaseId); + for (const document of documents) { + this.appendFanavaranAttachmentCandidate( + candidates, + seen, + submittedFiles, + { + path: (document as { path?: unknown }).path, + fileName: (document as { fileName?: unknown }).fileName, + source: `document:${String((document as { documentType?: unknown }).documentType ?? "unknown")}`, + }, + fileTypeId, + ); + } + + const carAngles = claimCase.media?.carAngles; + const angleEntries = + carAngles instanceof Map + ? Array.from(carAngles.entries()) + : carAngles && typeof carAngles === "object" + ? Object.entries(carAngles) + : []; + for (const [angleKey, image] of angleEntries) { + const capture = image as { path?: unknown; fileName?: unknown }; + this.appendFanavaranAttachmentCandidate( + candidates, + seen, + submittedFiles, + { + path: capture?.path, + fileName: capture?.fileName, + source: `capture:angle:${angleKey}`, + }, + fileTypeId, + ); + } + + const damagedParts = claimCase.media?.damagedParts; + const partEntries = Array.isArray(damagedParts) + ? damagedParts.map((part, index) => [String(index), part] as const) + : damagedParts && typeof damagedParts === "object" + ? Object.entries(damagedParts) + : []; + for (const [partKey, image] of partEntries) { + const capture = image as { path?: unknown; fileName?: unknown }; + this.appendFanavaranAttachmentCandidate( + candidates, + seen, + submittedFiles, + { + path: capture?.path, + fileName: capture?.fileName, + source: `capture:part:${partKey}`, + }, + fileTypeId, + ); + } + + return { claimCase, candidates }; + } + + async previewFanavaranAttachmentsV2( + claimCaseId: string, + clientKey: FanavaranClientKey, + ): Promise { + const { claimCase, candidates } = + await this.collectFanavaranAttachmentCandidates(claimCaseId, clientKey); + + return { + clientKey, + claimCaseId, + claimId: claimCase.claimId ?? null, + submitUrl: claimCase.claimId + ? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/files` + : null, + warning: claimCase.claimId + ? undefined + : "Fanavaran base claimId is required before attachment submit. Submit will retry base claim first.", + totalLocalImages: candidates.length, + alreadySubmitted: candidates.filter( + (candidate) => candidate.alreadySubmitted, + ).length, + pendingSubmit: candidates.filter( + (candidate) => !candidate.alreadySubmitted, + ).length, + attachments: candidates, + }; + } + + async submitFanavaranAttachmentsV2( + claimCaseId: string, + clientKey: FanavaranClientKey, + ): Promise { + const { candidates } = await this.collectFanavaranAttachmentCandidates( + claimCaseId, + clientKey, + ); + const pending = candidates.filter( + (candidate) => !candidate.alreadySubmitted, + ); + const results: FanavaranAttachmentSubmitResult[] = []; + + for (const candidate of pending) { + results.push( + await this.autoSubmitFanavaranAttachment( + claimCaseId, + { + path: candidate.path, + fileName: candidate.fileName, + source: candidate.source, + }, + { + clientKey, + auditSource: FanavaranAuditSource.SUBMIT, + }, + ), + ); + } + + return { + clientKey, + claimCaseId, + totalLocalImages: candidates.length, + skippedAlreadySubmitted: candidates.length - pending.length, + attempted: results.length, + submitted: results.filter((result) => result.submitted).length, + failed: results.filter( + (result) => result.attempted && !result.submitted && !result.skipped, + ).length, + skipped: results.filter((result) => result.skipped).length, + results, + }; + } + + private fanavaranLookupDefinition(name: string) { + const definition = FANAVARAN_REMOTE_LOOKUPS.find( + (item) => item.name === name, + ); + if (!definition) { + throw new Error(`Unknown Fanavaran lookup definition: ${name}`); + } + return definition; + } + + private async getFanavaranLookupRows( + clientKey: FanavaranClientKey, + name: string, + ): Promise { + const definition = this.fanavaranLookupDefinition(name); + const data = await this.fanavaranLookupService.getRemoteLookup( + clientKey, + definition.url, + definition.cacheFile, + ); + return Array.isArray(data) ? data : []; + } + + private normalizeFanavaranLookupText(value: unknown): string { + return String(value ?? "") + .toLowerCase() + .replace(/[ي]/g, "ی") + .replace(/[ك]/g, "ک") + .replace(/[\u200c\s_\-\/]+/g, "") + .replace(/[^\p{L}\p{N}]/gu, ""); + } + + private lookupRowId(row: unknown): number | null { + if (!row || typeof row !== "object") return null; + const raw = + (row as { Id?: unknown; id?: unknown; Code?: unknown }).Id ?? + (row as { id?: unknown }).id ?? + (row as { Code?: unknown }).Code; + const id = Number(raw); + return Number.isFinite(id) ? id : null; + } + + private lookupRowTextCandidates(row: unknown): string[] { + if (!row || typeof row !== "object") return []; + const r = row as Record; + return [ + r.Caption, + r.caption, + r.Title, + r.title, + r.Name, + r.name, + r.Text, + r.text, + r.Description, + r.description, + r.Value, + r.value, + ] + .filter((value): value is string => typeof value === "string") + .filter(Boolean); + } + + private findLookupIdByText(rows: unknown[], texts: unknown[]): number | null { + const wanted = texts + .map((text) => this.normalizeFanavaranLookupText(text)) + .filter(Boolean); + if (!wanted.length) return null; + + for (const row of rows) { + const rowTexts = this.lookupRowTextCandidates(row).map((text) => + this.normalizeFanavaranLookupText(text), + ); + if ( + rowTexts.some((rowText) => + wanted.some( + (want) => + rowText === want || + rowText.includes(want) || + want.includes(rowText), + ), + ) + ) { + return this.lookupRowId(row); + } + } + + return null; + } + + private parseFanavaranMoney(value: unknown): number { + if (value == null || value === "") return 0; + if (typeof value === "number" && Number.isFinite(value)) return value; + const normalized = String(value) + .replace(/[۰-۹]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 1728)) + .replace(/[٠-٩]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 1632)) + .replace(/,/g, "") + .trim(); + const parsed = Number(normalized); + return Number.isFinite(parsed) ? parsed : 0; + } + + private fanavaranPartLabel(part: any): string { + const damage = part?.carPartDamage; + if (damage && typeof damage === "object") { + const d = damage as { + label_fa?: unknown; + part?: unknown; + name?: unknown; + catalogKey?: unknown; + }; + return String( + d.label_fa ?? d.part ?? d.name ?? d.catalogKey ?? part?.partId ?? "", + ); + } + return String(damage ?? part?.partId ?? ""); + } + + private fanavaranPartLookupTexts(part: any): unknown[] { + const damage = part?.carPartDamage; + if (damage && typeof damage === "object") { + const d = damage as Record; + return [ + d.label_fa, + d.part, + d.name, + d.catalogKey, + part.partId, + this.fanavaranPartLabel(part), + ]; + } + return [damage, part?.partId, this.fanavaranPartLabel(part)]; + } + + private fanavaranDaghiWasteValue(daghi: unknown): number { + if (!daghi || typeof daghi !== "object") + return this.parseFanavaranMoney(daghi); + const d = daghi as { price?: unknown }; + return this.parseFanavaranMoney(d.price); + } + + private findPriceDropLineForPart(priceDrop: any, part: any): any { + const lines = Array.isArray(priceDrop?.partLines) + ? priceDrop.partLines + : []; + return lines.find((line: any) => { + const linePartId = parseCatalogPartIdInput(line?.partId); + const partId = parseCatalogPartIdInput(part?.partId); + return linePartId != null && partId != null && linePartId === partId; + }); + } + + private findDropAmountStatusId( + rows: unknown[], + priceDropTotal: number, + ): number | null { + const positiveTexts = + priceDropTotal > 0 + ? ["مشمول", "دارد", "افت", "has", "yes"] + : ["فاقد", "ندارد", "عدم", "بدون", "no"]; + return this.findLookupIdByText(rows, positiveTexts); + } + + private buildFanavaranExpertiseDmgSection(input: { + part: any; + priceDrop: any; + carComponents: unknown[]; + accidentLevels: unknown[]; + warnings: string[]; + }): Record { + const priceDropLine = this.findPriceDropLineForPart( + input.priceDrop, + input.part, + ); + const sectionId = this.findLookupIdByText( + input.carComponents, + this.fanavaranPartLookupTexts(input.part), + ); + const accidentLevel = this.findLookupIdByText(input.accidentLevels, [ + priceDropLine?.severity, + input.part?.typeOfDamage, + ]); + const label = this.fanavaranPartLabel(input.part); + + if (sectionId == null) { + input.warnings.push( + `No Fanavaran DmgSectionId mapping for part "${label}".`, + ); + } + if (accidentLevel == null) { + input.warnings.push( + `No Fanavaran AccidentLevel mapping for part "${label}".`, + ); + } + + return { + Desc: input.part?.typeOfDamage ?? label, + DmgSectionId: sectionId, + AccidentLevel: accidentLevel, + ComponentReplacementCost: this.parseFanavaranMoney(input.part?.price), + RepairWage: this.parseFanavaranMoney(input.part?.salary), + WasteValue: this.fanavaranDaghiWasteValue(input.part?.daghi), + }; + } + + private async buildFanavaranExpertisePayload(input: { + claimCase: any; + clientKey: FanavaranClientKey; + }): Promise<{ + payload: Record; + warnings: string[]; + replyKey?: string; + }> { + const profile = getFanavaranClientProfile(input.clientKey); + const active = getActiveV2ExpertReply(input.claimCase as any); + const warnings: string[] = []; + + if (!active?.parts?.length) { + warnings.push("No active damage expert reply with priced parts exists."); + } + + const reply = active?.reply as any; + const parts = (active?.parts ?? []).filter((part: any) => { + if (part.factorNeeded !== true) return true; + return this.parseFanavaranMoney(part.totalPayment) > 0; + }) as any[]; + + if (!parts.length) { + warnings.push("No submit-ready expert pricing lines exist yet."); + } + + const [ + inspectionPlaces, + dropAmountStatuses, + carComponents, + accidentLevels, + ] = await Promise.all([ + this.getFanavaranLookupRows(input.clientKey, "inspection-place"), + this.getFanavaranLookupRows(input.clientKey, "drop-amount-status"), + this.getFanavaranLookupRows(input.clientKey, "car-components"), + this.getFanavaranLookupRows(input.clientKey, "accident-level"), + ]); + + const priceDrop = input.claimCase.evaluation?.priceDrop ?? {}; + const priceDropTotal = this.parseFanavaranMoney(priceDrop?.total); + const inspectionPlaceId = this.findLookupIdByText(inspectionPlaces, [ + input.claimCase.evaluation?.visitLocation, + "محل بازدید", + "کارشناسی", + ]); + const dropAmountStatus = this.findDropAmountStatusId( + dropAmountStatuses, + priceDropTotal, + ); + + if (inspectionPlaceId == null) { + warnings.push( + "No Fanavaran InspectionPlaceId mapping could be resolved.", + ); + } + if (dropAmountStatus == null) { + warnings.push("No Fanavaran DropAmountStatus mapping could be resolved."); + } + + const dmgSections = parts.map((part) => + this.buildFanavaranExpertiseDmgSection({ + part, + priceDrop, + carComponents, + accidentLevels, + warnings, + }), + ); + const repairWage = dmgSections.reduce( + (sum, section) => sum + this.parseFanavaranMoney(section.RepairWage), + 0, + ); + const componentReplacementCost = dmgSections.reduce( + (sum, section) => + sum + this.parseFanavaranMoney(section.ComponentReplacementCost), + 0, + ); + const wasteValue = dmgSections.reduce( + (sum, section) => sum + this.parseFanavaranMoney(section.WasteValue), + 0, + ); + const submittedAt = reply?.submittedAt ?? new Date(); + + return { + replyKey: active?.replyKey, + warnings, + payload: { + ClaimExpertId: + reply?.expertProfileSnapshot?.fanavaranExpertId ?? + profile.defaults.ClaimExpertId, + ComponentReplacementCost: componentReplacementCost, + DmgAssessmentDate: this.convertToPersianDate(submittedAt), + DmgCaseId: input.claimCase.dmgCaseId ?? null, + InspectionPlaceId: inspectionPlaceId, + InspectionTime: this.getTime24Hour(submittedAt), + RepairWage: repairWage, + WasteValue: wasteValue, + WentDistanceByExpert: null, + DropAmountStatus: dropAmountStatus, + DropAmountAdditionsDeductions: priceDropTotal || 0, + ComponentReplacementTaxAndToll: 0, + DamagedVehicleCurrentPrice: + this.parseFanavaranMoney(priceDrop?.carPrice) || null, + DmgSections: dmgSections, + }, + }; + } + + private assertFanavaranExpertisePayloadReady( + payload: Record, + warnings: string[], + ): void { + const sections = Array.isArray(payload.DmgSections) + ? payload.DmgSections + : []; + if (!payload.DmgCaseId) warnings.push("DmgCaseId is required."); + if (!payload.InspectionPlaceId) + warnings.push("InspectionPlaceId is required."); + if (!payload.DropAmountStatus) + warnings.push("DropAmountStatus is required."); + if (!sections.length) + warnings.push("At least one DmgSections row is required."); + for (const [index, section] of sections.entries()) { + const row = section as Record; + if (!row.DmgSectionId) { + warnings.push(`DmgSections[${index}].DmgSectionId is required.`); + } + if (!row.AccidentLevel) { + warnings.push(`DmgSections[${index}].AccidentLevel is required.`); + } + } + if (warnings.length) { + throw new BadRequestException({ + message: "Fanavaran expertise payload is not ready.", + warnings: Array.from(new Set(warnings)), + }); + } + } + + async previewFanavaranExpertiseV2( + claimCaseId: string, + clientKey: FanavaranClientKey, + ): Promise { + const claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) throw new NotFoundException("Claim case not found"); + + const { payload, warnings, replyKey } = + await this.buildFanavaranExpertisePayload({ claimCase, clientKey }); + + return { + clientKey, + claimCaseId, + claimId: claimCase.claimId ?? null, + dmgCaseId: claimCase.dmgCaseId ?? null, + expertiseId: claimCase.expertiseId ?? null, + replyKey, + submitUrl: claimCase.claimId + ? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/expertise` + : null, + ready: warnings.length === 0, + warnings: Array.from(new Set(warnings)), + payload, + }; + } + + async submitFanavaranExpertiseV2( + claimCaseId: string, + clientKey: FanavaranClientKey, + ): Promise { + try { + const claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) throw new NotFoundException("Claim case not found"); + const { payload, warnings } = await this.buildFanavaranExpertisePayload({ + claimCase, + clientKey, + }); + this.assertFanavaranExpertisePayloadReady(payload, warnings); + return await this.executeFanavaranExpertiseSubmit( + claimCaseId, + clientKey, + payload, + FanavaranAuditSource.SUBMIT, + ); + } catch (error) { + if (error instanceof HttpException) throw error; + this.logger.error( + `[Fanavaran ${clientKey} V2] Error submitting expertise to Fanavaran`, + error, + ); + throw new BadGatewayException(this.extractFanavaranErrorMessage(error)); + } + } + + private async executeFanavaranExpertiseSubmit( + claimCaseId: string, + clientKey: FanavaranClientKey, + payload: Record, + auditSource: FanavaranAuditSource, + ): Promise { + const claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase?.claimId) { + throw new BadRequestException( + "Fanavaran claimId is required before submitting expertise", + ); + } + const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/expertise`; + const startedAt = Date.now(); + const auditSession: FanavaranAuditSession = { + trackingCode: this.fanavaranAuditService.generateTrackingCode(), + clientKey, + source: auditSource, + claimCaseId, + }; + + await this.fanavaranAuditService.recordStep({ + session: auditSession, + step: FanavaranAuditStep.SUBMIT_EXPERTISE, + status: FanavaranAuditStatus.STARTED, + requestUrl: url, + requestMeta: { claimId: claimCase.claimId, payload }, + }); + + try { + const response = await this.postFanavaranJson(url, payload, clientKey); + const expertiseId = response.data?.Id; + + await this.fanavaranAuditService.recordStep({ + session: auditSession, + step: FanavaranAuditStep.SUBMIT_EXPERTISE, + status: FanavaranAuditStatus.SUCCESS, + requestUrl: url, + httpStatus: response.status, + responseMeta: { claimId: claimCase.claimId, expertiseId }, + durationMs: Date.now() - startedAt, + }); + + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + ...(expertiseId !== undefined ? { expertiseId } : {}), + "fanavaranSync.expertise": { + status: "success", + lastTriedAt: new Date(), + claimId: claimCase.claimId, + dmgCaseId: payload.DmgCaseId, + expertiseId, + response: response.data, + }, + }, + }); + + return response.data; + } catch (error) { + await this.fanavaranAuditService.recordStep({ + session: auditSession, + step: FanavaranAuditStep.SUBMIT_EXPERTISE, + status: FanavaranAuditStatus.FAILURE, + requestUrl: url, + httpStatus: isAxiosError(error) ? error.response?.status : undefined, + errorMessage: this.fanavaranAuditService.extractErrorMessage(error), + errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error), + durationMs: Date.now() - startedAt, + }); + throw error; + } + } + + async autoSubmitFanavaranExpertiseOnExpertReply( + claimCaseId: string, + ): Promise { + const clientKey = resolveFanavaranClientKey(); + const logPrefix = `[Fanavaran ${clientKey} V2 Expertise Auto] claimCaseId=${claimCaseId}`; + try { + let claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) { + return { + attempted: false, + submitted: false, + warning: "Claim case not found for Fanavaran expertise submit.", + }; + } + if (claimCase.expertiseId != null) { + return { + attempted: false, + submitted: false, + skipped: true, + skipReason: "Expertise already submitted to Fanavaran", + claimId: claimCase.claimId, + dmgCaseId: claimCase.dmgCaseId, + expertiseId: claimCase.expertiseId, + }; + } + + if (!claimCase.claimId) { + await this.autoSubmitToFanavaranV2OnClaimCreated(claimCaseId); + claimCase = await this.claimCaseDbService.findById(claimCaseId); + } + if (!claimCase?.dmgCaseId) { + await this.autoSubmitFanavaranDamageCaseOnOuterPartsSelected( + claimCaseId, + claimCase?.damage?.selectedParts ?? [], + ); + claimCase = await this.claimCaseDbService.findById(claimCaseId); + } + + const { payload, warnings } = await this.buildFanavaranExpertisePayload({ + claimCase, + clientKey, + }); + this.assertFanavaranExpertisePayloadReady(payload, warnings); + + const fanavaranResponse = await this.executeFanavaranExpertiseSubmit( + claimCaseId, + clientKey, + payload, + FanavaranAuditSource.AUTO_SUBMIT, + ); + const expertiseId = fanavaranResponse?.Id; + + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $push: { + history: { + type: "FANAVARAN_EXPERTISE_AUTO_SUBMIT_SUCCEEDED", + actor: { actorType: "system" }, + timestamp: new Date(), + metadata: { + clientKey, + claimId: claimCase?.claimId, + dmgCaseId: claimCase?.dmgCaseId, + expertiseId, + }, + }, + }, + }); + + return { + attempted: true, + submitted: true, + claimId: claimCase?.claimId, + dmgCaseId: claimCase?.dmgCaseId, + expertiseId, + fanavaranResponse, + }; + } catch (error) { + const warning = this.extractFanavaranErrorMessage(error); + this.logger.warn(`${logPrefix} Expertise auto-submit failed: ${warning}`); + try { + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + "fanavaranSync.expertise.status": "failed", + "fanavaranSync.expertise.lastTriedAt": new Date(), + "fanavaranSync.expertise.lastError": warning, + }, + $push: { + history: { + type: "FANAVARAN_EXPERTISE_AUTO_SUBMIT_FAILED", + actor: { actorType: "system" }, + timestamp: new Date(), + metadata: { clientKey, error: warning }, + }, + }, + }); + } catch (historyError) { + this.logger.error( + `${logPrefix} Failed to record Fanavaran expertise failure history`, + historyError, + ); + } + return { + attempted: true, + submitted: false, + warning, + }; + } + } + + private withFanavaranAutoSubmitMessage( + baseMessage: string, + fanavaran: FanavaranAutoSubmitResult, + ): string { + if (fanavaran.submitted) { + return `${baseMessage} The initial third-party case was sent to Fanavaran successfully.`; + } + if (fanavaran.warning) { + return `${baseMessage} ${fanavaran.warning}`; + } + return baseMessage; + } + + private async getFanavaranAutoSubmitSkipReason( + claimCase: any, + ): Promise { + if (claimCase.claimId != null || claimCase.claimNo != null) { + return "Already submitted to Fanavaran"; + } + + if (!claimCase.blameRequestId) { + return "Blame case not linked to claim case"; + } + + const blameCase = await this.blameRequestDbService.findById( + claimCase.blameRequestId, + ); + if (!blameCase) { + return "Blame case not found"; + } + + if (blameCase.type !== BlameRequestType.THIRD_PARTY) { + return "Fanavaran third-party financial submit only applies to THIRD_PARTY claims"; + } + + return null; + } + + private async postFanavaranJson( + url: string, + payload: Record, + clientKey: FanavaranClientKey, + ) { + const profile = getFanavaranClientProfile(clientKey); + const appToken = await this.getAppToken(profile.auth); + const authenticationToken = await this.login(appToken, profile.auth); + + return await firstValueFrom( + this.httpService.post(url, payload, { + headers: { + authenticationToken, + CorpId: profile.auth.corpId, + ContractId: profile.auth.contractId, + Location: profile.auth.location, + "Content-Type": "application/json", + }, + }), + ); + } + + private async postFanavaranMultipart( + url: string, + content: Record, + files: Array<{ path: string; fileName: string }>, + clientKey: FanavaranClientKey, + ) { + const profile = getFanavaranClientProfile(clientKey); + const appToken = await this.getAppToken(profile.auth); + const authenticationToken = await this.login(appToken, profile.auth); + const form = new FormData(); + + form.append("content", JSON.stringify(content), { + contentType: "application/json", + }); + for (const file of files) { + form.append("files", createReadStream(file.path), { + filename: file.fileName, + }); + } + + return await firstValueFrom( + this.httpService.post(url, form, { + headers: { + ...form.getHeaders(), + authenticationToken, + CorpId: profile.auth.corpId, + ContractId: profile.auth.contractId, + Location: profile.auth.location, + }, + maxBodyLength: Infinity, + maxContentLength: Infinity, + }), + ); + } + + async autoSubmitFanavaranAttachment( + claimCaseId: string, + file: { path?: string | null; fileName?: string | null; source?: string }, + options?: { + clientKey?: FanavaranClientKey; + auditSource?: FanavaranAuditSource; + }, + ): Promise { + const clientKey = options?.clientKey ?? resolveFanavaranClientKey(); + const auditSource = + options?.auditSource ?? FanavaranAuditSource.AUTO_SUBMIT; + const logPrefix = `[Fanavaran ${clientKey} V2 Attachment Auto] claimCaseId=${claimCaseId}`; + const filePath = file.path ? String(file.path) : ""; + const fileName = file.fileName + ? String(file.fileName) + : filePath + ? basename(filePath) + : ""; + + try { + if (!filePath || !fileName) { + return { + attempted: false, + submitted: false, + skipped: true, + skipReason: "Attachment file path or filename is missing", + }; + } + if (!existsSync(filePath)) { + return { + attempted: false, + submitted: false, + skipped: true, + skipReason: `Attachment file does not exist: ${filePath}`, + fileName, + }; + } + + let claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) { + return { + attempted: false, + submitted: false, + warning: "Claim case not found for Fanavaran attachment submit.", + fileName, + }; + } + + if (!claimCase.claimId) { + if (options?.clientKey) { + await this.executeFanavaranV2Submit(claimCaseId, clientKey); + } else { + await this.autoSubmitToFanavaranV2OnClaimCreated(claimCaseId); + } + claimCase = await this.claimCaseDbService.findById(claimCaseId); + } + + if (!claimCase?.claimId) { + const warning = + "Fanavaran base claim is missing, so attachment was not submitted."; + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + "fanavaranSync.attachments.status": "skipped", + "fanavaranSync.attachments.lastTriedAt": new Date(), + "fanavaranSync.attachments.lastError": warning, + }, + }); + return { + attempted: false, + submitted: false, + skipped: true, + skipReason: warning, + warning, + fileName, + }; + } + + const profile = getFanavaranClientProfile(clientKey); + const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/files`; + const content = { + FileName: fileName, + FileTypeId: profile.defaults.ClaimFileTypeId, + Files: [ + { FileName: fileName, FileTypeId: profile.defaults.ClaimFileTypeId }, + ], + }; + const auditSession: FanavaranAuditSession = { + trackingCode: this.fanavaranAuditService.generateTrackingCode(), + clientKey, + source: auditSource, + claimCaseId, + }; + const startedAt = Date.now(); + + await this.fanavaranAuditService.recordStep({ + session: auditSession, + step: FanavaranAuditStep.SUBMIT_ATTACHMENT, + status: FanavaranAuditStatus.STARTED, + requestUrl: url, + requestMeta: { + claimId: claimCase.claimId, + fileName, + source: file.source, + content, + }, + }); + + const response = await this.postFanavaranMultipart( + url, + content, + [{ path: filePath, fileName }], + clientKey, + ); + const fileId = response.data?.Id; + + await this.fanavaranAuditService.recordStep({ + session: auditSession, + step: FanavaranAuditStep.SUBMIT_ATTACHMENT, + status: FanavaranAuditStatus.SUCCESS, + requestUrl: url, + httpStatus: response.status, + responseMeta: { claimId: claimCase.claimId, fileId, fileName }, + durationMs: Date.now() - startedAt, + }); + + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + "fanavaranSync.attachments.status": "success", + "fanavaranSync.attachments.lastTriedAt": new Date(), + "fanavaranSync.attachments.claimId": claimCase.claimId, + "fanavaranSync.attachments.response": response.data, + }, + $push: { + "fanavaranSync.attachments.files": { + fileId, + fileName, + source: file.source, + response: response.data, + submittedAt: new Date(), + }, + history: { + type: "FANAVARAN_ATTACHMENT_AUTO_SUBMIT_SUCCEEDED", + actor: { actorType: "system" }, + timestamp: new Date(), + metadata: { + clientKey, + claimId: claimCase.claimId, + fileId, + fileName, + source: file.source, + }, + }, + }, + }); + + return { + attempted: true, + submitted: true, + claimId: claimCase.claimId, + fileId, + fileName, + fanavaranResponse: response.data, + }; + } catch (error) { + const warning = this.extractFanavaranErrorMessage(error); + this.logger.warn( + `${logPrefix} Attachment auto-submit failed: ${warning}`, + ); + + try { + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + "fanavaranSync.attachments.status": "failed", + "fanavaranSync.attachments.lastTriedAt": new Date(), + "fanavaranSync.attachments.lastError": warning, + }, + $push: { + history: { + type: "FANAVARAN_ATTACHMENT_AUTO_SUBMIT_FAILED", + actor: { actorType: "system" }, + timestamp: new Date(), + metadata: { + clientKey, + error: warning, + fileName, + source: file.source, + }, + }, + }, + }); + } catch (historyError) { + this.logger.error( + `${logPrefix} Failed to record Fanavaran attachment failure history`, + historyError, + ); + } + + return { + attempted: true, + submitted: false, + warning, + fileName, + }; + } + } + + private async executeFanavaranDamageCaseSubmit( + claimCaseId: string, + clientKey: FanavaranClientKey, + selectedParts?: unknown[], + auditSource: FanavaranAuditSource = FanavaranAuditSource.AUTO_SUBMIT, + ): Promise { + const profile = getFanavaranClientProfile(clientKey); + const claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) { + throw new NotFoundException("Claim case not found"); + } + if (!claimCase.claimId) { + throw new BadRequestException( + "Fanavaran claimId is required before submitting damage case", + ); + } + if (!claimCase.blameRequestId) { + throw new BadRequestException("Blame case not linked to claim case"); + } + + const blameCase = await this.blameRequestDbService.findById( + claimCase.blameRequestId, + ); + if (!blameCase) { + throw new NotFoundException("Blame case not found"); + } + if (blameCase.type !== BlameRequestType.THIRD_PARTY) { + throw new BadRequestException( + "Fanavaran damage-case submit only applies to THIRD_PARTY claims", + ); + } + + const payload = this.buildFanavaranDamageCasePayload({ + claimCase, + blameCase, + selectedParts: selectedParts ?? claimCase.damage?.selectedParts ?? [], + defaults: profile.defaults, + }); + const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/dmg-cases`; + const startedAt = Date.now(); + const auditSession: FanavaranAuditSession = { + trackingCode: this.fanavaranAuditService.generateTrackingCode(), + clientKey, + source: auditSource, + claimCaseId, + }; + + await this.fanavaranAuditService.recordStep({ + session: auditSession, + step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE, + status: FanavaranAuditStatus.STARTED, + requestUrl: url, + requestMeta: { claimId: claimCase.claimId, payload }, + }); + + try { + const response = await this.postFanavaranJson(url, payload, clientKey); + const dmgCaseId = response.data?.Id; + + await this.fanavaranAuditService.recordStep({ + session: auditSession, + step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE, + status: FanavaranAuditStatus.SUCCESS, + requestUrl: url, + httpStatus: response.status, + responseMeta: { dmgCaseId, claimId: claimCase.claimId }, + durationMs: Date.now() - startedAt, + }); + + if (dmgCaseId !== undefined) { + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + dmgCaseId, + "fanavaranSync.damageCase": { + status: "success", + lastTriedAt: new Date(), + claimId: claimCase.claimId, + dmgCaseId, + response: response.data, + }, + }, + }); + } + + return response.data; + } catch (error) { + await this.fanavaranAuditService.recordStep({ + session: auditSession, + step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE, + status: FanavaranAuditStatus.FAILURE, + requestUrl: url, + httpStatus: isAxiosError(error) ? error.response?.status : undefined, + errorMessage: this.fanavaranAuditService.extractErrorMessage(error), + errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error), + durationMs: Date.now() - startedAt, + }); + throw error; + } + } + + async autoSubmitFanavaranDamageCaseOnOuterPartsSelected( + claimCaseId: string, + selectedParts: unknown[], + ): Promise { + const clientKey = resolveFanavaranClientKey(); + const logPrefix = `[Fanavaran ${clientKey} V2 DamageCase Auto] claimCaseId=${claimCaseId}`; + + try { + let claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) { + return { + attempted: false, + submitted: false, + warning: "Claim case not found for Fanavaran damage-case submit.", + }; + } + if (claimCase.dmgCaseId != null) { + return { + attempted: false, + submitted: false, + skipped: true, + skipReason: "Damage case already submitted to Fanavaran", + claimId: claimCase.claimId, + dmgCaseId: claimCase.dmgCaseId, + }; + } + + if (!claimCase.claimId) { + await this.autoSubmitToFanavaranV2OnClaimCreated(claimCaseId); + claimCase = await this.claimCaseDbService.findById(claimCaseId); + } + + if (!claimCase?.claimId) { + const warning = + "Fanavaran base claim is missing, so damage case was not submitted."; + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + "fanavaranSync.damageCase": { + status: "skipped", + lastTriedAt: new Date(), + lastError: warning, + }, + }, + }); + return { + attempted: false, + submitted: false, + skipped: true, + skipReason: warning, + warning, + }; + } + + const fanavaranResponse = await this.executeFanavaranDamageCaseSubmit( + claimCaseId, + clientKey, + selectedParts, + ); + const dmgCaseId = fanavaranResponse?.Id; + + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $push: { + history: { + type: "FANAVARAN_DAMAGE_CASE_AUTO_SUBMIT_SUCCEEDED", + actor: { actorType: "system" }, + timestamp: new Date(), + metadata: { + clientKey, + claimId: claimCase.claimId, + dmgCaseId, + }, + }, + }, + }); + + return { + attempted: true, + submitted: true, + claimId: claimCase.claimId, + dmgCaseId, + fanavaranResponse, + }; + } catch (error) { + const warning = this.extractFanavaranErrorMessage(error); + this.logger.warn( + `${logPrefix} Damage-case auto-submit failed: ${warning}`, + ); + + try { + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $set: { + "fanavaranSync.damageCase": { + status: "failed", + lastTriedAt: new Date(), + lastError: warning, + }, + }, + $push: { + history: { + type: "FANAVARAN_DAMAGE_CASE_AUTO_SUBMIT_FAILED", + actor: { actorType: "system" }, + timestamp: new Date(), + metadata: { clientKey, error: warning }, + }, + }, + }); + } catch (historyError) { + this.logger.error( + `${logPrefix} Failed to record Fanavaran damage-case failure history`, + historyError, + ); + } + + return { + attempted: true, + submitted: false, + warning, + }; + } + } + + /** + * Auto-submit the minimum third-party Fanavaran claim as soon as the local + * claim case exists. This preserves progress in Fanavaran even if parties stop + * later in the damage-assessment flow. + */ + async autoSubmitToFanavaranV2OnClaimCreated( + claimCaseId: string, + ): Promise { + const clientKey = resolveFanavaranClientKey(); + const logPrefix = `[Fanavaran ${clientKey} V2 Early Auto] claimCaseId=${claimCaseId}`; + + try { + const claimCase = await this.claimCaseDbService.findById(claimCaseId); + if (!claimCase) { + return { + attempted: false, + submitted: false, + warning: + "Claim case not found for Fanavaran early submit. Retry manually when available.", + }; + } + + const skipReason = await this.getFanavaranAutoSubmitSkipReason(claimCase); + if (skipReason) { + return { + attempted: false, + submitted: false, + skipped: true, + skipReason, + claimId: claimCase.claimId, + claimNo: claimCase.claimNo, + }; + } + + const fanavaranResponse = await this.executeFanavaranV2Submit( + claimCaseId, + clientKey, + ); + + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $push: { + history: { + type: "FANAVARAN_EARLY_AUTO_SUBMIT_SUCCEEDED", + actor: { actorType: "system" }, + timestamp: new Date(), + metadata: { + clientKey, + claimNo: fanavaranResponse?.ClaimNo, + claimId: fanavaranResponse?.Id, + }, + }, + }, + }); + + return { + attempted: true, + submitted: true, + claimNo: fanavaranResponse?.ClaimNo, + claimId: fanavaranResponse?.Id, + fanavaranResponse, + }; + } catch (error) { + const warning = this.extractFanavaranErrorMessage(error); + this.logger.warn(`${logPrefix} Early auto-submit failed: ${warning}`); + + try { + await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { + $push: { + history: { + type: "FANAVARAN_EARLY_AUTO_SUBMIT_FAILED", + actor: { actorType: "system" }, + timestamp: new Date(), + metadata: { clientKey, error: warning }, + }, + }, + }); + } catch (historyError) { + this.logger.error( + `${logPrefix} Failed to record Fanavaran early auto-submit failure history`, + historyError, + ); + } + + return { + attempted: true, + submitted: false, + warning: `${warning} Initial case was not sent to Fanavaran. Retry manually via POST ${fanavaranSubmitPath(clientKey, claimCaseId)}.`, + }; + } + } + /** * Auto-submit to Fanavaran when a claim case reaches COMPLETED. * Uses FANAVARAN_CLIENT env (or CLIENT_ID fallback) to pick the tenant. @@ -4291,12 +5954,13 @@ export class ClaimRequestManagementService { }; } - if (claimCase.claimId != null || claimCase.claimNo != null) { + const skipReason = await this.getFanavaranAutoSubmitSkipReason(claimCase); + if (skipReason) { return { attempted: false, submitted: false, skipped: true, - skipReason: "Already submitted to Fanavaran", + skipReason, claimId: claimCase.claimId, claimNo: claimCase.claimNo, }; @@ -4846,11 +6510,20 @@ export class ClaimRequestManagementService { `Claim created: ${newClaim._id} from blame: ${blameRequestId}`, ); + const fanavaran = await this.autoSubmitToFanavaranV2OnClaimCreated( + String(newClaim._id), + ); + const message = this.withFanavaranAutoSubmitMessage( + "Claim request created successfully", + fanavaran, + ); + return { claimRequestId: String(newClaim._id), publicId: blameRequest.publicId, status: ClaimCaseStatus.SELECTING_OUTER_PARTS, - message: "Claim request created successfully", + message, + fanavaran, }; } catch (error) { if (error instanceof HttpException) throw error; @@ -4959,12 +6632,19 @@ export class ClaimRequestManagementService { this.logger.log( `Claim created by field expert: ${newClaim._id} from blame: ${blameRequestId}`, ); + const fanavaran = await this.autoSubmitToFanavaranV2OnClaimCreated( + String(newClaim._id), + ); + const message = this.withFanavaranAutoSubmitMessage( + "Claim created successfully. You can now fill claim data on behalf of the damaged party.", + fanavaran, + ); return { claimRequestId: String(newClaim._id), publicId: blameRequest.publicId, status: ClaimCaseStatus.SELECTING_OUTER_PARTS, - message: - "Claim created successfully. You can now fill claim data on behalf of the damaged party.", + message, + fanavaran, }; } @@ -5248,11 +6928,19 @@ export class ClaimRequestManagementService { }, ], } as any); + const fanavaran = await this.autoSubmitToFanavaranV2OnClaimCreated( + String(newClaim._id), + ); + const message = this.withFanavaranAutoSubmitMessage( + "Claim created successfully. Registrar can now fill claim data.", + fanavaran, + ); return { claimRequestId: String(newClaim._id), publicId: blameRequest.publicId, status: ClaimCaseStatus.SELECTING_OUTER_PARTS, - message: "Claim created successfully. Registrar can now fill claim data.", + message, + fanavaran, }; } @@ -5342,12 +7030,19 @@ export class ClaimRequestManagementService { }, ], } as any); + const fanavaran = await this.autoSubmitToFanavaranV2OnClaimCreated( + String(newClaim._id), + ); + const message = this.withFanavaranAutoSubmitMessage( + "Claim created successfully. Registrar can now fill claim data on behalf of the damaged party.", + fanavaran, + ); return { claimRequestId: String(newClaim._id), publicId: blameRequest.publicId, status: ClaimCaseStatus.SELECTING_OUTER_PARTS, - message: - "Claim created successfully. Registrar can now fill claim data on behalf of the damaged party.", + message, + fanavaran, }; } @@ -5544,6 +7239,20 @@ export class ClaimRequestManagementService { `Outer parts selected for claim ${claimRequestId}: ${selectedPartDocs?.length} parts`, ); + const fanavaranDamageCase = + await this.autoSubmitFanavaranDamageCaseOnOuterPartsSelected( + claimRequestId, + selectedPartDocs, + ); + + let message = + "Outer parts selected successfully. Please proceed to select other parts and provide bank information."; + if (fanavaranDamageCase.submitted) { + message += " The damage case was sent to Fanavaran successfully."; + } else if (fanavaranDamageCase.warning) { + message += ` ${fanavaranDamageCase.warning}`; + } + // 7. Return response return { claimRequestId: updatedClaim._id.toString(), @@ -5552,8 +7261,8 @@ export class ClaimRequestManagementService { selectedPartIds, currentStep: ClaimWorkflowStep.SELECT_OTHER_PARTS, nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, - message: - "Outer parts selected successfully. Please proceed to select other parts and provide bank information.", + message, + fanavaranDamageCase, }; } catch (error) { if (error instanceof HttpException) throw error; @@ -6532,6 +8241,14 @@ export class ClaimRequestManagementService { await this.claimCaseDbService.findById(claimRequestId); const expertResendComplete = !!refreshed?.evaluation?.damageExpertResend?.fulfilledAt; + const fanavaranAttachment = await this.autoSubmitFanavaranAttachment( + claimRequestId, + { + path: file.path, + fileName: file.filename, + source: `document:${body.documentKey}:resend`, + }, + ); return { claimRequestId: claimCase._id.toString(), documentKey: body.documentKey, @@ -6544,6 +8261,7 @@ export class ClaimRequestManagementService { message: expertResendComplete ? "Expert resend requirements are complete. Your claim is back in the damage expert queue." : "Document uploaded for expert resend.", + fanavaranAttachment, }; } @@ -6578,6 +8296,14 @@ export class ClaimRequestManagementService { : ClaimWorkflowStep.USER_SUBMISSION_COMPLETE : ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, message, + fanavaranAttachment: await this.autoSubmitFanavaranAttachment( + claimRequestId, + { + path: file.path, + fileName: file.filename, + source: `document:${body.documentKey}`, + }, + ), }; } catch (error) { if (error instanceof HttpException) throw error; @@ -6806,6 +8532,14 @@ export class ClaimRequestManagementService { await this.claimCaseDbService.findById(claimRequestId); const expertResendComplete = !!refreshed?.evaluation?.damageExpertResend?.fulfilledAt; + const fanavaranAttachment = await this.autoSubmitFanavaranAttachment( + claimRequestId, + { + path: file.path, + fileName: file.filename, + source: `capture:${body.captureType}:${body.captureKey}:resend`, + }, + ); return { claimRequestId: claimCase._id.toString(), captureType: body.captureType, @@ -6819,6 +8553,7 @@ export class ClaimRequestManagementService { message: expertResendComplete ? "Expert resend requirements are complete. Your claim is back in the damage expert queue." : "Part photo uploaded for expert resend.", + fanavaranAttachment, }; } @@ -6876,6 +8611,14 @@ export class ClaimRequestManagementService { ? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS : ClaimWorkflowStep.CAPTURE_PART_DAMAGES, message, + fanavaranAttachment: await this.autoSubmitFanavaranAttachment( + claimRequestId, + { + path: file.path, + fileName: file.filename, + source: `capture:${body.captureType}:${body.captureKey}`, + }, + ), }; } catch (error) { if (error instanceof HttpException) throw error; diff --git a/src/claim-request-management/dto/capture-part-v2.dto.ts b/src/claim-request-management/dto/capture-part-v2.dto.ts index 12ef6f6..497854b 100644 --- a/src/claim-request-management/dto/capture-part-v2.dto.ts +++ b/src/claim-request-management/dto/capture-part-v2.dto.ts @@ -1,34 +1,34 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsEnum, IsNotEmpty, IsString } from "class-validator"; /** * V2 DTO for capturing car angle or damaged part */ export class CapturePartV2Dto { @ApiProperty({ - description: 'Type of capture: angle or part', - example: 'angle', - enum: ['angle', 'part'], + description: "Type of capture: angle or part", + example: "angle", + enum: ["angle", "part"], }) - @IsNotEmpty({ message: 'Capture type is required' }) - @IsEnum(['angle', 'part'], { + @IsNotEmpty({ message: "Capture type is required" }) + @IsEnum(["angle", "part"], { message: 'Capture type must be either "angle" or "part"', }) - captureType: 'angle' | 'part'; + captureType: "angle" | "part"; @ApiProperty({ description: 'When captureType is angle: front | back | left | right. When part: catalog id as string (e.g. "101"), 0-based index (e.g. "0"), or full catalog key (e.g. left_backfender). Prefer id or index for parts.', - example: 'front', + example: "front", }) - @IsNotEmpty({ message: 'Capture key is required' }) - @IsString({ message: 'Capture key must be a string' }) + @IsNotEmpty({ message: "Capture key is required" }) + @IsString({ message: "Capture key must be a string" }) captureKey: string; @ApiProperty({ - type: 'string', - format: 'binary', - description: 'Image file (JPG, PNG)', + type: "string", + format: "binary", + description: "Image file (JPG, PNG)", }) file: Express.Multer.File; } @@ -38,51 +38,58 @@ export class CapturePartV2Dto { */ export class CapturePartV2ResponseDto { @ApiProperty({ - description: 'Claim request ID', - example: '507f1f77bcf86cd799439011', + description: "Claim request ID", + example: "507f1f77bcf86cd799439011", }) claimRequestId: string; @ApiProperty({ - description: 'Type of capture', - example: 'angle', + description: "Type of capture", + example: "angle", }) captureType: string; @ApiProperty({ - description: 'Key of what was captured', - example: 'front', + description: "Key of what was captured", + example: "front", }) captureKey: string; @ApiProperty({ - description: 'File URL', - example: 'http://localhost:3000/files/captures/front-1234567890.jpg', + description: "File URL", + example: "http://localhost:3000/files/captures/front-1234567890.jpg", }) fileUrl: string; @ApiProperty({ - description: 'Whether all captures are now complete', + description: "Whether all captures are now complete", example: false, }) allCapturesComplete: boolean; @ApiProperty({ - description: 'Current workflow step', - example: 'CAPTURE_PART_DAMAGES', + description: "Current workflow step", + example: "CAPTURE_PART_DAMAGES", }) currentStep: string; @ApiProperty({ - description: 'Success message', - example: 'Angle captured successfully. 6 captures remaining.', + description: "Success message", + example: "Angle captured successfully. 6 captures remaining.", }) message: string; @ApiPropertyOptional({ - description: 'True when expert-requested part resends are complete and the claim returned to the expert queue.', + description: + "True when expert-requested part resends are complete and the claim returned to the expert queue.", }) expertResendComplete?: boolean; + + @ApiPropertyOptional({ + description: + "Best-effort Fanavaran attachment upload result. Local capture still succeeds when this contains a warning.", + }) + fanavaranAttachment?: unknown; } /** @@ -90,20 +97,20 @@ export class CapturePartV2ResponseDto { */ export class VideoCaptureV2ResponseDto { @ApiProperty({ - description: 'Claim case ID', - example: '507f1f77bcf86cd799439011', + description: "Claim case ID", + example: "507f1f77bcf86cd799439011", }) claimRequestId: string; @ApiProperty({ - description: 'ID of the stored video document (claim-video-capture)', - example: '507f1f77bcf86cd799439012', + description: "ID of the stored video document (claim-video-capture)", + example: "507f1f77bcf86cd799439012", }) videoId: string; @ApiProperty({ - description: 'Success message', - example: 'Video capture uploaded successfully.', + description: "Success message", + example: "Video capture uploaded successfully.", }) message: string; } diff --git a/src/claim-request-management/dto/create-claim-v2.dto.ts b/src/claim-request-management/dto/create-claim-v2.dto.ts index 8b753d9..c6d0c72 100644 --- a/src/claim-request-management/dto/create-claim-v2.dto.ts +++ b/src/claim-request-management/dto/create-claim-v2.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; export class CreateClaimFromBlameResponseDto { @ApiProperty({ @@ -23,4 +23,10 @@ export class CreateClaimFromBlameResponseDto { example: "Claim request created successfully", }) message: string; + + @ApiPropertyOptional({ + description: + "Best-effort Fanavaran early submit result. Claim creation still succeeds when this contains a warning.", + }) + fanavaran?: unknown; } diff --git a/src/claim-request-management/dto/select-outer-parts-v2.dto.ts b/src/claim-request-management/dto/select-outer-parts-v2.dto.ts index a1ef916..abeda1a 100644 --- a/src/claim-request-management/dto/select-outer-parts-v2.dto.ts +++ b/src/claim-request-management/dto/select-outer-parts-v2.dto.ts @@ -1,5 +1,13 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsEnum, IsNotEmpty, ArrayMinSize, ArrayUnique, IsOptional, IsInt } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { + IsArray, + IsEnum, + IsNotEmpty, + ArrayMinSize, + ArrayUnique, + IsOptional, + IsInt, +} from "class-validator"; import { ClaimVehicleTypeV2, OuterPartSideV2, @@ -12,27 +20,27 @@ import { DamageSelectedPartV2BodyDto } from "./damage-selected-part-v2.dto"; */ export enum OuterCarPart { // Hood - HOOD = 'hood', - + HOOD = "hood", + // Doors - FRONT_RIGHT_DOOR = 'front_right_door', - FRONT_LEFT_DOOR = 'front_left_door', - REAR_RIGHT_DOOR = 'rear_right_door', - REAR_LEFT_DOOR = 'rear_left_door', - + FRONT_RIGHT_DOOR = "front_right_door", + FRONT_LEFT_DOOR = "front_left_door", + REAR_RIGHT_DOOR = "rear_right_door", + REAR_LEFT_DOOR = "rear_left_door", + // Bumpers - FRONT_BUMPER = 'front_bumper', - REAR_BUMPER = 'rear_bumper', - + FRONT_BUMPER = "front_bumper", + REAR_BUMPER = "rear_bumper", + // Fenders - FRONT_RIGHT_FENDER = 'front_right_fender', - FRONT_LEFT_FENDER = 'front_left_fender', - REAR_RIGHT_FENDER = 'rear_right_fender', - REAR_LEFT_FENDER = 'rear_left_fender', - + FRONT_RIGHT_FENDER = "front_right_fender", + FRONT_LEFT_FENDER = "front_left_fender", + REAR_RIGHT_FENDER = "rear_right_fender", + REAR_LEFT_FENDER = "rear_left_fender", + // Trunk & Roof - TRUNK = 'trunk', - ROOF = 'roof', + TRUNK = "trunk", + ROOF = "roof", } /** @@ -41,38 +49,38 @@ export enum OuterCarPart { */ export class SelectOuterPartsV2Dto { @ApiProperty({ - description: 'Array of selected damaged outer car parts', - example: ['hood', 'front_right_door', 'rear_bumper', 'roof'], + description: "Array of selected damaged outer car parts", + example: ["hood", "front_right_door", "rear_bumper", "roof"], enum: OuterCarPart, isArray: true, minItems: 1, maxItems: 13, }) @IsOptional() - @IsArray({ message: 'selectedParts must be an array' }) - @ArrayMinSize(1, { message: 'At least one damaged part must be selected' }) - @ArrayUnique({ message: 'Duplicate parts are not allowed' }) + @IsArray({ message: "selectedParts must be an array" }) + @ArrayMinSize(1, { message: "At least one damaged part must be selected" }) + @ArrayUnique({ message: "Duplicate parts are not allowed" }) @IsEnum(OuterCarPart, { each: true, - message: 'Invalid part name. Must be one of the valid outer car parts', + message: "Invalid part name. Must be one of the valid outer car parts", }) selectedParts?: OuterCarPart[]; @ApiProperty({ - description: 'Selected outer part IDs from catalog', + description: "Selected outer part IDs from catalog", example: [9, 10, 4], type: [Number], required: false, }) @IsOptional() - @IsArray({ message: 'selectedPartIds must be an array' }) - @ArrayMinSize(1, { message: 'At least one part ID must be selected' }) - @ArrayUnique({ message: 'Duplicate part IDs are not allowed' }) - @IsInt({ each: true, message: 'Each selected part ID must be an integer' }) + @IsArray({ message: "selectedPartIds must be an array" }) + @ArrayMinSize(1, { message: "At least one part ID must be selected" }) + @ArrayUnique({ message: "Duplicate part IDs are not allowed" }) + @IsInt({ each: true, message: "Each selected part ID must be an integer" }) selectedPartIds?: number[]; @ApiProperty({ - description: 'Vehicle type for validating available outer parts', + description: "Vehicle type for validating available outer parts", enum: ClaimVehicleTypeV2, required: true, }) @@ -86,14 +94,14 @@ export class SelectOuterPartsV2Dto { */ export class SelectOuterPartsV2ResponseDto { @ApiProperty({ - description: 'Claim request ID', - example: '507f1f77bcf86cd799439011', + description: "Claim request ID", + example: "507f1f77bcf86cd799439011", }) claimRequestId: string; @ApiProperty({ - description: 'Public ID shared across blame and claim', - example: 'A14235', + description: "Public ID shared across blame and claim", + example: "A14235", }) publicId: string; @@ -105,29 +113,36 @@ export class SelectOuterPartsV2ResponseDto { selectedParts: DamageSelectedPartV2BodyDto[]; @ApiProperty({ - description: 'Selected part IDs', + description: "Selected part IDs", example: [9, 7, 11], type: [Number], }) selectedPartIds: number[]; @ApiProperty({ - description: 'Current workflow step', - example: 'SELECT_OUTER_PARTS', + description: "Current workflow step", + example: "SELECT_OUTER_PARTS", }) currentStep: string; @ApiProperty({ - description: 'Next possible workflow step', - example: 'SELECT_OTHER_PARTS', + description: "Next possible workflow step", + example: "SELECT_OTHER_PARTS", }) nextStep: string; @ApiProperty({ - description: 'Success message', - example: 'Outer parts selected successfully. Please proceed to select other parts.', + description: "Success message", + example: + "Outer parts selected successfully. Please proceed to select other parts.", }) message: string; + + @ApiPropertyOptional({ + description: + "Best-effort Fanavaran damage-case submit result. Selecting parts still succeeds when this contains a warning.", + }) + fanavaranDamageCase?: unknown; } export class SetClaimVehicleTypeV2Dto { diff --git a/src/claim-request-management/dto/upload-document-v2.dto.ts b/src/claim-request-management/dto/upload-document-v2.dto.ts index 64c5e68..61141ca 100644 --- a/src/claim-request-management/dto/upload-document-v2.dto.ts +++ b/src/claim-request-management/dto/upload-document-v2.dto.ts @@ -1,26 +1,26 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum'; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsEnum, IsNotEmpty, IsString } from "class-validator"; +import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum"; /** * V2 DTO for uploading required document */ export class UploadRequiredDocumentV2Dto { @ApiProperty({ - description: 'Document type/key', - example: 'car_green_card', + description: "Document type/key", + example: "car_green_card", enum: ClaimRequiredDocumentType, }) - @IsNotEmpty({ message: 'Document key is required' }) + @IsNotEmpty({ message: "Document key is required" }) @IsEnum(ClaimRequiredDocumentType, { - message: 'Invalid document type', + message: "Invalid document type", }) documentKey: ClaimRequiredDocumentType; @ApiProperty({ - type: 'string', - format: 'binary', - description: 'Image file (JPG, PNG, PDF)', + type: "string", + format: "binary", + description: "Image file (JPG, PNG, PDF)", }) file: Express.Multer.File; } @@ -30,43 +30,51 @@ export class UploadRequiredDocumentV2Dto { */ export class UploadRequiredDocumentV2ResponseDto { @ApiProperty({ - description: 'Claim request ID', - example: '507f1f77bcf86cd799439011', + description: "Claim request ID", + example: "507f1f77bcf86cd799439011", }) claimRequestId: string; @ApiProperty({ - description: 'Document key that was uploaded', - example: 'car_green_card', + description: "Document key that was uploaded", + example: "car_green_card", }) documentKey: string; @ApiProperty({ - description: 'File URL', - example: 'http://localhost:3000/files/documents/car-green-card-1234567890.jpg', + description: "File URL", + example: + "http://localhost:3000/files/documents/car-green-card-1234567890.jpg", }) fileUrl: string; @ApiProperty({ - description: 'Whether all required documents are now uploaded', + description: "Whether all required documents are now uploaded", example: false, }) allDocumentsUploaded: boolean; @ApiProperty({ - description: 'Current workflow step', - example: 'UPLOAD_REQUIRED_DOCUMENTS', + description: "Current workflow step", + example: "UPLOAD_REQUIRED_DOCUMENTS", }) currentStep: string; @ApiProperty({ - description: 'Success message', - example: 'Document uploaded successfully. 12 documents remaining.', + description: "Success message", + example: "Document uploaded successfully. 12 documents remaining.", }) message: string; @ApiPropertyOptional({ - description: 'True when the owner finished every damage-expert resend requirement and the claim is back in the expert queue.', + description: + "True when the owner finished every damage-expert resend requirement and the claim is back in the expert queue.", }) expertResendComplete?: boolean; + + @ApiPropertyOptional({ + description: + "Best-effort Fanavaran attachment upload result. Local document upload still succeeds when this contains a warning.", + }) + fanavaranAttachment?: unknown; } diff --git a/src/claim-request-management/entites/schema/claim-cases.schema.ts b/src/claim-request-management/entites/schema/claim-cases.schema.ts index 9832942..c66db0b 100644 --- a/src/claim-request-management/entites/schema/claim-cases.schema.ts +++ b/src/claim-request-management/entites/schema/claim-cases.schema.ts @@ -58,6 +58,55 @@ export class RequiredDocumentRef { export const RequiredDocumentRefSchema = SchemaFactory.createForClass(RequiredDocumentRef); +@Schema({ _id: false }) +export class FanavaranSyncStage { + @Prop({ type: String }) + status?: "pending" | "success" | "failed" | "skipped"; + + @Prop({ type: Date }) + lastTriedAt?: Date; + + @Prop({ type: String }) + lastError?: string; + + @Prop({ type: Number }) + claimId?: number; + + @Prop({ type: Number }) + claimNo?: number; + + @Prop({ type: Number }) + dmgCaseId?: number; + + @Prop({ type: Number }) + expertiseId?: number; + + @Prop({ type: [MongooseSchema.Types.Mixed], default: [] }) + files?: unknown[]; + + @Prop({ type: MongooseSchema.Types.Mixed }) + response?: unknown; +} +export const FanavaranSyncStageSchema = + SchemaFactory.createForClass(FanavaranSyncStage); + +@Schema({ _id: false }) +export class FanavaranSyncState { + @Prop({ type: FanavaranSyncStageSchema }) + baseClaim?: FanavaranSyncStage; + + @Prop({ type: FanavaranSyncStageSchema }) + damageCase?: FanavaranSyncStage; + + @Prop({ type: FanavaranSyncStageSchema }) + attachments?: FanavaranSyncStage; + + @Prop({ type: FanavaranSyncStageSchema }) + expertise?: FanavaranSyncStage; +} +export const FanavaranSyncStateSchema = + SchemaFactory.createForClass(FanavaranSyncState); + @Schema({ collection: "claimCases", timestamps: true, @@ -142,6 +191,15 @@ export class ClaimCase { @Prop({ type: Number }) claimId?: number; + @Prop({ type: Number }) + dmgCaseId?: number; + + @Prop({ type: Number }) + expertiseId?: number; + + @Prop({ type: FanavaranSyncStateSchema, default: () => ({}) }) + fanavaranSync?: FanavaranSyncState; + @Prop({ type: ClaimDamageSelectionSchema, default: () => ({}) }) damage?: ClaimDamageSelection; diff --git a/src/core/config/fanavaran-client.config.ts b/src/core/config/fanavaran-client.config.ts index f1c591a..26da272 100644 --- a/src/core/config/fanavaran-client.config.ts +++ b/src/core/config/fanavaran-client.config.ts @@ -5,7 +5,9 @@ export const FANAVARAN_CLIENT_KEYS: readonly FanavaranClientKey[] = [ "tejaratno", ] as const; -export function isFanavaranClientKey(value: string): value is FanavaranClientKey { +export function isFanavaranClientKey( + value: string, +): value is FanavaranClientKey { const normalized = value?.trim().toLowerCase(); return normalized === "parsian" || normalized === "tejaratno"; } @@ -38,6 +40,13 @@ export interface FanavaranPayloadDefaults { CompensationReferenceId: number; CulpritLicenceTypeId: number; CulpritTypeId: number; + DmgCaseTypeId: number; + DmgHistoryStatus: number; + PlaqueKindId: number; + PlaqueSampleId: number; + DriverIsOwner: number; + FaultPercent: number; + ClaimFileTypeId: number; } export interface FanavaranClientProfile { @@ -69,6 +78,13 @@ const FANAVARAN_CLIENT_PROFILES: Record< CompensationReferenceId: 167, CulpritLicenceTypeId: 2, CulpritTypeId: 337, + DmgCaseTypeId: 175, + DmgHistoryStatus: 5214, + PlaqueKindId: 8, + PlaqueSampleId: 10, + DriverIsOwner: 0, + FaultPercent: 100, + ClaimFileTypeId: 70, }, }, parsian: { @@ -90,6 +106,13 @@ const FANAVARAN_CLIENT_PROFILES: Record< CompensationReferenceId: 167, CulpritLicenceTypeId: 2, CulpritTypeId: 337, + DmgCaseTypeId: 175, + DmgHistoryStatus: 5214, + PlaqueKindId: 8, + PlaqueSampleId: 10, + DriverIsOwner: 0, + FaultPercent: 100, + ClaimFileTypeId: 70, }, }, }; @@ -127,14 +150,14 @@ export function fanavaranPreviewPath( clientKey: FanavaranClientKey, claimCaseId: string, ): string { - return `/v2/fanavaran/${clientKey}/claim-cases/${claimCaseId}`; + return `/v2/fanavaran/${clientKey}/claim-cases/${claimCaseId}/base-claim/preview`; } export function fanavaranSubmitPath( clientKey: FanavaranClientKey, claimCaseId: string, ): string { - return `/v2/fanavaran/${clientKey}/claim-cases/${claimCaseId}`; + return `/v2/fanavaran/${clientKey}/claim-cases/${claimCaseId}/base-claim/submit`; } export function fanavaranManualSubmitPath(claimCaseId: string): string { diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 2b1fb7c..611723e 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -19,6 +19,7 @@ import { ClaimRequestManagementDbService } from "src/claim-request-management/en import { ClaimRequestManagementService, FanavaranAutoSubmitResult, + FanavaranExpertiseSubmitResult, } from "src/claim-request-management/claim-request-management.service"; import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service"; import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service"; @@ -289,7 +290,7 @@ export class ExpertClaimService { private appendFanavaranAutoSubmitToMessage( baseMessage: string, - fanavaran: FanavaranAutoSubmitResult, + fanavaran: FanavaranAutoSubmitResult | FanavaranExpertiseSubmitResult, ): string { if (fanavaran.submitted) { return `${baseMessage} The claim was sent to Fanavaran successfully.`; @@ -2371,16 +2372,24 @@ export class ExpertClaimService { await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted( claimRequestId, ); + const fanavaranExpertise = + await this.claimRequestManagementService.autoSubmitFanavaranExpertiseOnExpertReply( + claimRequestId, + ); return { message: this.appendFanavaranAutoSubmitToMessage( - "Factors were reviewed with expert repricing on rejected lines. The claim is completed without an owner signature (temporary policy; may require owner acceptance later).", - fanavaran, + this.appendFanavaranAutoSubmitToMessage( + "Factors were reviewed with expert repricing on rejected lines. The claim is completed without an owner signature (temporary policy; may require owner acceptance later).", + fanavaran, + ), + fanavaranExpertise, ), claimRequestId, claimStatus: ClaimStatus.APPROVED, caseStatus: ClaimCaseStatus.COMPLETED, outcome: "REJECTED_REPRICED_AUTO_COMPLETED", fanavaran, + fanavaranExpertise, }; } @@ -2397,17 +2406,25 @@ export class ExpertClaimService { await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted( claimRequestId, ); + const fanavaranExpertise = + await this.claimRequestManagementService.autoSubmitFanavaranExpertiseOnExpertReply( + claimRequestId, + ); return { message: this.appendFanavaranAutoSubmitToMessage( - "All factors were approved by the expert. The claim is completed without an additional owner signature.", - fanavaran, + this.appendFanavaranAutoSubmitToMessage( + "All factors were approved by the expert. The claim is completed without an additional owner signature.", + fanavaran, + ), + fanavaranExpertise, ), claimRequestId, claimStatus: ClaimStatus.APPROVED, caseStatus: ClaimCaseStatus.COMPLETED, outcome: "ALL_APPROVED_AUTO_COMPLETED", fanavaran, + fanavaranExpertise, }; } @@ -3239,6 +3256,18 @@ export class ExpertClaimService { }); } + const fanavaranExpertise = !needsFactorUpload + ? await this.claimRequestManagementService.autoSubmitFanavaranExpertiseOnExpertReply( + claimRequestId, + ) + : { + attempted: false, + submitted: false, + skipped: true, + skipReason: + "Expertise submit waits until factor-needed repair lines are validated.", + }; + return { claimRequestId, status: nextCaseStatus, @@ -3251,6 +3280,7 @@ export class ExpertClaimService { mixedPricingAndFactors: mixedFactorAndPrice, allPartsFactorNeeded: !!needsFactorUpload && !!allFactorLines, isFinalReplyAfterObjection, + fanavaranExpertise, }; } diff --git a/src/fanavaran/fanavaran-lookup.config.ts b/src/fanavaran/fanavaran-lookup.config.ts index 597f3b3..e7aff0c 100644 --- a/src/fanavaran/fanavaran-lookup.config.ts +++ b/src/fanavaran/fanavaran-lookup.config.ts @@ -41,6 +41,31 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [ url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-culprit-type`, cacheFile: "accident-culprit-type.json", }, + { + name: "inspection-place", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/inspection-place`, + cacheFile: "inspection-place.json", + }, + { + name: "drop-amount-status", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/drop-amount-status`, + cacheFile: "drop-amount-status.json", + }, + { + name: "car-components", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/car-components`, + cacheFile: "car-components.json", + }, + { + name: "accident-level", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-level`, + cacheFile: "accident-level.json", + }, + { + name: "expert-status", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/expert-status`, + cacheFile: "expert-status.json", + }, ]; export const TEJARAT_STATIC_ACCIDENT_FILES = { diff --git a/src/fanavaran/fanavaran.controller.ts b/src/fanavaran/fanavaran.controller.ts index 70ff83d..2de4f65 100644 --- a/src/fanavaran/fanavaran.controller.ts +++ b/src/fanavaran/fanavaran.controller.ts @@ -54,11 +54,11 @@ export class FanavaranController { }; } - @Get(":client/claim-cases/:claimCaseId") + @Get(":client/claim-cases/:claimCaseId/base-claim/preview") @ApiOperation({ - summary: "Preview Fanavaran submit payload (V2)", + summary: "Preview Fanavaran base claim create payload", description: - "Builds the third-party-car-financial-claims body from claimCases + blameCases without calling Fanavaran.", + "Builds the GEN.03 third-party-car-financial-claims payload from local claimCases + blameCases without creating a Fanavaran claim.", }) @ApiParam({ name: "client", @@ -87,11 +87,11 @@ export class FanavaranController { ); } - @Post(":client/claim-cases/:claimCaseId") + @Post(":client/claim-cases/:claimCaseId/base-claim/submit") @ApiOperation({ - summary: "Submit claim to Fanavaran (V2)", + summary: "Submit Fanavaran base claim create request", description: - "Authenticates with the selected client credentials and submits the mapped claimCases + blameCases payload.", + "Authenticates with the selected tenant credentials and submits the GEN.03 base claim create request. Stores returned Id as claimId and ClaimNo when present.", }) @ApiParam({ name: "client", @@ -113,6 +113,162 @@ export class FanavaranController { ); } + @Get(":client/claim-cases/:claimCaseId/damage-case/preview") + @ApiOperation({ + summary: "Preview Fanavaran damage-case payload", + description: + "Builds the GEN.12 damaged vehicle/person case payload without calling Fanavaran. Requires selected damaged parts; submit requires a Fanavaran claimId.", + }) + @ApiParam({ + name: "client", + description: "Fanavaran tenant key", + enum: ["parsian", "tejaratno"], + }) + @ApiParam({ + name: "claimCaseId", + description: "Claim case MongoDB ObjectId", + }) + async previewDamageCase( + @Param("client") client: string, + @Param("claimCaseId") claimCaseId: string, + ) { + const clientKey = this.parseClientParam(client); + return await this.claimRequestManagementService.previewFanavaranDamageCaseV2( + claimCaseId, + clientKey, + ); + } + + @Post(":client/claim-cases/:claimCaseId/damage-case/submit") + @ApiOperation({ + summary: "Submit Fanavaran damage-case request", + description: + "Submits the GEN.12 dmg-cases request for the already-created Fanavaran claim and stores returned Id as local dmgCaseId.", + }) + @ApiParam({ + name: "client", + description: "Fanavaran tenant key", + enum: ["parsian", "tejaratno"], + }) + @ApiParam({ + name: "claimCaseId", + description: "Claim case MongoDB ObjectId", + }) + async submitDamageCase( + @Param("client") client: string, + @Param("claimCaseId") claimCaseId: string, + ) { + const clientKey = this.parseClientParam(client); + return await this.claimRequestManagementService.submitFanavaranDamageCaseV2( + claimCaseId, + clientKey, + ); + } + + @Get(":client/claim-cases/:claimCaseId/attachments/preview") + @ApiOperation({ + summary: "Preview Fanavaran attachment upload plan", + description: + "Lists local required-document and captured damage images that would be sent to GEN.07. Shows which images are already recorded as uploaded to Fanavaran.", + }) + @ApiParam({ + name: "client", + description: "Fanavaran tenant key", + enum: ["parsian", "tejaratno"], + }) + @ApiParam({ + name: "claimCaseId", + description: "Claim case MongoDB ObjectId", + }) + async previewAttachments( + @Param("client") client: string, + @Param("claimCaseId") claimCaseId: string, + ) { + const clientKey = this.parseClientParam(client); + return await this.claimRequestManagementService.previewFanavaranAttachmentsV2( + claimCaseId, + clientKey, + ); + } + + @Post(":client/claim-cases/:claimCaseId/attachments/submit") + @ApiOperation({ + summary: "Submit missing Fanavaran attachments", + description: + "Uploads every local image that does not already have a recorded successful GEN.07 Fanavaran file upload. Uses one multipart request per image.", + }) + @ApiParam({ + name: "client", + description: "Fanavaran tenant key", + enum: ["parsian", "tejaratno"], + }) + @ApiParam({ + name: "claimCaseId", + description: "Claim case MongoDB ObjectId", + }) + async submitAttachments( + @Param("client") client: string, + @Param("claimCaseId") claimCaseId: string, + ) { + const clientKey = this.parseClientParam(client); + return await this.claimRequestManagementService.submitFanavaranAttachmentsV2( + claimCaseId, + clientKey, + ); + } + + @Get(":client/claim-cases/:claimCaseId/expertise/preview") + @ApiOperation({ + summary: "Preview Fanavaran expertise payload", + description: + "Builds the GEN.08 expertise payload from the active damage expert reply, price-drop data, and Fanavaran lookup mappings without calling Fanavaran.", + }) + @ApiParam({ + name: "client", + description: "Fanavaran tenant key", + enum: ["parsian", "tejaratno"], + }) + @ApiParam({ + name: "claimCaseId", + description: "Claim case MongoDB ObjectId", + }) + async previewExpertise( + @Param("client") client: string, + @Param("claimCaseId") claimCaseId: string, + ) { + const clientKey = this.parseClientParam(client); + return await this.claimRequestManagementService.previewFanavaranExpertiseV2( + claimCaseId, + clientKey, + ); + } + + @Post(":client/claim-cases/:claimCaseId/expertise/submit") + @ApiOperation({ + summary: "Submit Fanavaran expertise request", + description: + "Submits the GEN.08 expertise payload for a claim with existing Fanavaran claimId and dmgCaseId. Stores returned Id as local expertiseId.", + }) + @ApiParam({ + name: "client", + description: "Fanavaran tenant key", + enum: ["parsian", "tejaratno"], + }) + @ApiParam({ + name: "claimCaseId", + description: "Claim case MongoDB ObjectId", + }) + async submitExpertise( + @Param("client") client: string, + @Param("claimCaseId") claimCaseId: string, + ) { + const clientKey = this.parseClientParam(client); + return await this.claimRequestManagementService.submitFanavaranExpertiseV2( + claimCaseId, + clientKey, + ); + } + private parseClientParam(client: string) { if (!isFanavaranClientKey(client)) { throw new BadRequestException( diff --git a/src/fanavaran/schema/fanavaran-audit-log.schema.ts b/src/fanavaran/schema/fanavaran-audit-log.schema.ts index aad2666..84c8fa1 100644 --- a/src/fanavaran/schema/fanavaran-audit-log.schema.ts +++ b/src/fanavaran/schema/fanavaran-audit-log.schema.ts @@ -8,6 +8,9 @@ export enum FanavaranAuditStep { POLICY_INQUIRY = "POLICY_INQUIRY", BUILD_PAYLOAD = "BUILD_PAYLOAD", SUBMIT_CLAIM = "SUBMIT_CLAIM", + SUBMIT_DAMAGE_CASE = "SUBMIT_DAMAGE_CASE", + SUBMIT_ATTACHMENT = "SUBMIT_ATTACHMENT", + SUBMIT_EXPERTISE = "SUBMIT_EXPERTISE", } export enum FanavaranAuditStatus { @@ -31,13 +34,23 @@ export class FanavaranAuditLog { @Prop({ type: String, required: true, enum: FanavaranAuditStep, index: true }) step: FanavaranAuditStep; - @Prop({ type: String, required: true, enum: FanavaranAuditStatus, index: true }) + @Prop({ + type: String, + required: true, + enum: FanavaranAuditStatus, + index: true, + }) status: FanavaranAuditStatus; @Prop({ type: String, required: true, index: true }) clientKey: FanavaranClientKey; - @Prop({ type: String, required: true, enum: FanavaranAuditSource, index: true }) + @Prop({ + type: String, + required: true, + enum: FanavaranAuditSource, + index: true, + }) source: FanavaranAuditSource; @Prop({ type: Types.ObjectId, required: false, index: true }) diff --git a/src/lookups/lookups.controller.ts b/src/lookups/lookups.controller.ts index 6223343..bdb6e97 100644 --- a/src/lookups/lookups.controller.ts +++ b/src/lookups/lookups.controller.ts @@ -1,5 +1,10 @@ -import { Controller, Get } from "@nestjs/common"; -import { ApiOkResponse, ApiTags } from "@nestjs/swagger"; +import { Controller, Get, Param } from "@nestjs/common"; +import { + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from "@nestjs/swagger"; import { LookupsService } from "./lookups.service"; @ApiTags("lookups") @@ -96,9 +101,113 @@ export class LookupsController { return await this.lookupsService.getAccidentCulpritType(); } + @Get("inspection-place") + @ApiOperation({ + summary: "Fanavaran GEN.08 inspection place lookup", + description: + "Returns values for expertise payload field InspectionPlaceId from car/code-list/inspection-place.", + }) + @ApiOkResponse({ + description: "Returns Fanavaran inspection place lookup data", + schema: { type: "array", items: { type: "object" } }, + }) + async getInspectionPlace() { + return await this.lookupsService.getInspectionPlace(); + } + + @Get("drop-amount-status") + @ApiOperation({ + summary: "Fanavaran GEN.08 drop amount status lookup", + description: + "Returns values for expertise payload field DropAmountStatus from car/code-list/drop-amount-status.", + }) + @ApiOkResponse({ + description: "Returns Fanavaran drop amount status lookup data", + schema: { type: "array", items: { type: "object" } }, + }) + async getDropAmountStatus() { + return await this.lookupsService.getDropAmountStatus(); + } + + @Get("car-components") + @ApiOperation({ + summary: "Fanavaran GEN.08 damaged section lookup", + description: + "Returns values for DmgSections[].DmgSectionId from car/base-info/car-components.", + }) + @ApiOkResponse({ + description: "Returns Fanavaran car component lookup data", + schema: { type: "array", items: { type: "object" } }, + }) + async getCarComponents() { + return await this.lookupsService.getCarComponents(); + } + + @Get("accident-level") + @ApiOperation({ + summary: "Fanavaran GEN.08 accident level lookup", + description: + "Returns values for DmgSections[].AccidentLevel from car/code-list/accident-level.", + }) + @ApiOkResponse({ + description: "Returns Fanavaran accident level lookup data", + schema: { type: "array", items: { type: "object" } }, + }) + async getAccidentLevel() { + return await this.lookupsService.getAccidentLevel(); + } + + @Get("expert-status") + @ApiOperation({ + summary: "Fanavaran GEN.08 expert status lookup", + description: + "Returns values for expertise response field Status from car/code-list/expert-status.", + }) + @ApiOkResponse({ + description: "Returns Fanavaran expert status lookup data", + schema: { type: "array", items: { type: "object" } }, + }) + async getExpertStatus() { + return await this.lookupsService.getExpertStatus(); + } + + @Get("fanavaran") + @ApiOperation({ + summary: "List configured Fanavaran remote lookups", + description: + "Returns the lookup names available through /lookups/fanavaran/{lookupName}.", + }) + async listFanavaranRemoteLookups() { + return this.lookupsService.listFanavaranRemoteLookups().map((lookup) => ({ + name: lookup.name, + cacheFile: lookup.cacheFile, + url: lookup.url, + })); + } + + @Get("fanavaran/:lookupName") + @ApiOperation({ + summary: "Fetch a Fanavaran remote lookup by name", + description: + "Generic cached Fanavaran lookup fetcher for configured lookup names, including GEN.08 expertise lookups.", + }) + @ApiParam({ + name: "lookupName", + description: + "Configured Fanavaran lookup name, for example inspection-place, drop-amount-status, car-components, accident-level, expert-status", + }) + @ApiOkResponse({ + description: "Returns cached or live Fanavaran lookup data", + schema: { type: "array", items: { type: "object" } }, + }) + async getFanavaranRemoteLookup(@Param("lookupName") lookupName: string) { + return await this.lookupsService.getClientRemoteLookup(lookupName); + } + @Get("accident-way") @ApiOkResponse({ - description: "Returns accident way options for the add-accident-fields step", + description: + "Returns accident way options for the add-accident-fields step", schema: { type: "array", items: { @@ -205,4 +314,3 @@ export class LookupsController { return await this.lookupsService.getAccidentFields(); } } - diff --git a/src/lookups/lookups.service.ts b/src/lookups/lookups.service.ts index 5551cc3..9b46426 100644 --- a/src/lookups/lookups.service.ts +++ b/src/lookups/lookups.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { resolveFanavaranClientKey } from "src/core/config/fanavaran-client.config"; import { FANAVARAN_REMOTE_LOOKUPS, + type FanavaranRemoteLookupDefinition, TEJARAT_STATIC_ACCIDENT_FILES, } from "src/fanavaran/fanavaran-lookup.config"; import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service"; @@ -31,10 +32,14 @@ export class LookupsService { return resolveFanavaranClientKey(); } + listFanavaranRemoteLookups(): FanavaranRemoteLookupDefinition[] { + return FANAVARAN_REMOTE_LOOKUPS; + } + private findRemoteLookup(name: string) { const lookup = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name); if (!lookup) { - throw new Error(`Unknown Fanavaran remote lookup: ${name}`); + throw new NotFoundException(`Unknown Fanavaran remote lookup: ${name}`); } return lookup; } @@ -52,7 +57,7 @@ export class LookupsService { return doc.response; } - private async getClientRemoteLookup(lookupName: string): Promise { + async getClientRemoteLookup(lookupName: string): Promise { const clientKey = this.activeClientKey(); const definition = this.findRemoteLookup(lookupName); @@ -90,6 +95,26 @@ export class LookupsService { return await this.getClientRemoteLookup("accident-culprit-type"); } + async getInspectionPlace(): Promise { + return await this.getClientRemoteLookup("inspection-place"); + } + + async getDropAmountStatus(): Promise { + return await this.getClientRemoteLookup("drop-amount-status"); + } + + async getCarComponents(): Promise { + return await this.getClientRemoteLookup("car-components"); + } + + async getAccidentLevel(): Promise { + return await this.getClientRemoteLookup("accident-level"); + } + + async getExpertStatus(): Promise { + return await this.getClientRemoteLookup("expert-status"); + } + async getAccidentWay(): Promise<{ id: number; label: string }[]> { const clientKey = this.activeClientKey(); const fileName = TEJARAT_STATIC_ACCIDENT_FILES.accidentWay; @@ -129,10 +154,9 @@ export class LookupsService { if (clientKey === "parsian") { const cacheFile = "accident-reason-options.json"; - const cached = - await this.fanavaranLookupService.readCacheFile< - { id: number; label: string; fanavaran: number }[] - >(clientKey, cacheFile); + const cached = await this.fanavaranLookupService.readCacheFile< + { id: number; label: string; fanavaran: number }[] + >(clientKey, cacheFile); if (cached) { return cached; }