Merge upstream/main into main

Resolve conflicts while keeping field-expert lock/view fixes and user
party-access query improvements from the fork.
This commit is contained in:
2026-06-18 18:27:01 +03:30
6 changed files with 839 additions and 97 deletions

View File

@@ -151,6 +151,17 @@ import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs";
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
export interface FanavaranAutoSubmitResult {
attempted: boolean;
submitted: boolean;
skipped?: boolean;
skipReason?: string;
warning?: string;
claimNo?: number;
claimId?: number;
fanavaranResponse?: unknown;
}
@Injectable()
export class ClaimRequestManagementService {
private readonly logger = new Logger(ClaimRequestManagementService.name);
@@ -174,6 +185,41 @@ export class ClaimRequestManagementService {
private readonly CONTRACT_ID = "263";
private readonly LOCATION = "100";
private readonly PARSIAN_FANAVARAN_CONFIG = {
appName: "ParsianService",
secret: "P@r30@n$erv!ce",
username: "ParsianServiceUser",
password: "P@r30@n123",
corpId: "543",
contractId: "28",
location: "210050",
} as const;
/** Tejaratno hardcoded Fanavaran lookup defaults (legacy). */
private readonly TEJARATNO_FANAVARAN_DEFAULTS = {
AccidentCityId: 701,
AccidentReportTypeId: 155,
AccidentVehicleUsedId: 1,
ClaimExpertId: 1589,
CompensationReferenceId: 167,
CulpritLicenceTypeId: 2,
CulpritTypeId: 337,
} as const;
/** Parsian-validated Fanavaran lookup defaults (verified via direct submit). */
private readonly PARSIAN_FANAVARAN_DEFAULTS = {
AccidentCityId: 701,
AccidentReportTypeId: 155,
AccidentVehicleUsedId: 1,
ClaimExpertId: 154,
CompensationReferenceId: 167,
CulpritLicenceTypeId: 2,
CulpritTypeId: 337,
} as const;
private readonly MAP_IR_API_KEY =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2In0.eyJhdWQiOiIyMTcxOCIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2IiwiaWF0IjoxNjgwNjA4NTkxLCJuYmYiOjE2ODA2MDg1OTEsImV4cCI6MTY4MzIwMDU5MSwic3ViIjoiIiwic2NvcGVzIjpbImJhc2ljIl19.rTviLd8b5yTHUDa3ODZyva593eMnL0d3XPg3sKkZxMOf_jNIH6lFQyIfbId-wsd1EAdsOdsL3CME_Y8t332PWJbxMNgnEq4Rf2IkClkvkSx6Sb5_4bmlhBM75zw2SmccvgbFUn4xkTOw0FT4vABC2Y3-MKctjMpmO8QOrVULSKt4psrmQhr7hBu7YRDnAAEc6muZ1VpRvdB1kqNKddoSIrfDaq6aDRJ-BNbGRAaFFvP_kH4cgSCKV4dU0TknL3mRKUiVy6_TDkjtzAN8fE2wsdvNo2pGTJPzKFsR2ipgGNTvB__g3bOnVpKsgFXPBH0e_Qa7ff1tZ3VGWy3jRNh9Lg";
constructor(
private readonly blameDbService: RequestManagementDbService,
private readonly claimDbService: ClaimRequestManagementDbService,
@@ -3170,15 +3216,13 @@ export class ClaimRequestManagementService {
lon: number,
): Promise<string> {
try {
const apiKey =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2In0.eyJhdWQiOiIyMTcxOCIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2IiwiaWF0IjoxNjgwNjA4NTkxLCJuYmYiOjE2ODA2MDg1OTEsImV4cCI6MTY4MzIwMDU5MSwic3ViIjoiIiwic2NvcGVzIjpbImJhc2ljIl19.rTviLd8b5yTHUDa3ODZyva593eMnL0d3XPg3sKkZxMOf_jNIH6lFQyIfbId-wsd1EAdsOdsL3CME_Y8t332PWJbxMNgnEq4Rf2IkClkvkSx6Sb5_4bmlhBM75zw2SmccvgbFUn4xkTOw0FT4vABC2Y3-MKctjMpmO8QOrVULSKt4psrmQhr7hBu7YRDnAAEc6muZ1VpRvdB1kqNKddoSIrfDaq6aDRJ-BNbGRAaFFvP_kH4cgSCKV4dU0TknL3mRKUiVy6_TDkjtzAN8fE2wsdvNo2pGTJPzKFsR2ipgGNTvB__g3bOnVpKsgFXPBH0e_Qa7ff1tZ3VGWy3jRNh9Lg";
const response = await firstValueFrom(
this.httpService.get(
`https://map.ir/fast-reverse?lat=${lat}&lon=${lon}`,
{
headers: {
accept: "application/json",
"x-api-key": apiKey,
"x-api-key": this.MAP_IR_API_KEY,
},
},
),
@@ -3226,6 +3270,140 @@ export class ClaimRequestManagementService {
return ids;
}
private getTejaratnoFanavaranConfig() {
return {
appName: this.APP_NAME,
secret: this.SECRET,
username: this.USERNAME,
password: this.PASSWORD,
corpId: this.CORP_ID,
contractId: this.CONTRACT_ID,
location: this.LOCATION,
};
}
private resolveAccidentCauseId(accidentReason?: {
id?: string | number | null;
fanavaran?: number | null;
}): number | null {
const fanavaranValue = accidentReason?.fanavaran;
const idValue = accidentReason?.id;
if (fanavaranValue !== undefined && fanavaranValue !== null) {
if (fanavaranValue === 0) {
return idValue !== undefined && idValue !== null ? Number(idValue) : null;
}
return fanavaranValue;
}
return null;
}
private applyFanavaranDefaultFields(result: Record<string, unknown>): void {
result.AccidentCulpritId = null;
result.CouponNo = null;
result.CourtArchiveNo = null;
result.CulpritLicenceCityId = null;
result.CulpritLicenceCountryId = null;
result.CulpritLicenceForeignCityName = null;
result.CulpritLicenceIssuDate = "1394/10/13";
result.CulpritLicenceNo = "1124242";
result.DamagedCount = 1;
result.DmgAssessorFirstCreationTime = null;
result.EntryDate = null;
result.IsLicenseMatchWithVehicleKind = 1;
result.HasOtherCulprit = 0;
result.IsAccidentOutOfBorder = 0;
result.IsFatalAccident = 0;
result.IsPlaqueChanged = 0;
result.PlaqueCityId = null;
result.PlaqueKindId = null;
result.PlaqueLeftNo = null;
result.PlaqueMiddleCodeId = null;
result.PlaqueNo = null;
result.PlaqueRightNo = null;
result.PlaqueSampleId = null;
result.PlaqueSerial = null;
result.PoliceOfficerId = 1;
result.PoliceReportDesc = null;
result.PoliceReportSeri = null;
result.PoliceReportSerial = null;
result.PolicyId = null;
result.PreviousPolicyEndDate = "";
result.TrackingCode = null;
result.IsLicenseReplacement = 0;
result.UnknownCulpritCauseId = null;
}
private async buildFanavaranSubmitPayload(input: {
accidentReason?: { id?: string | number | null; fanavaran?: number | null };
createdAt?: Date | string;
location?: { lat?: number; lon?: number };
damageParts?: any[];
resolvePolicyId: () => Promise<number | null>;
logPrefix: string;
defaults?: {
AccidentCityId: number;
AccidentReportTypeId: number;
AccidentVehicleUsedId: number;
ClaimExpertId: number;
CompensationReferenceId: number;
CulpritLicenceTypeId: number;
CulpritTypeId: number;
};
}): Promise<Record<string, unknown>> {
const lookupDefaults =
input.defaults ?? this.TEJARATNO_FANAVARAN_DEFAULTS;
const result: Record<string, unknown> = {
...lookupDefaults,
AccidentCauseId: this.resolveAccidentCauseId(input.accidentReason),
};
if (input.accidentReason && result.AccidentCauseId === null) {
this.logger.warn(
`${input.logPrefix} accidentReason exists but fanavaran is null or undefined`,
);
} else if (!input.accidentReason) {
this.logger.error(
`${input.logPrefix} expert decision / accidentReason is missing`,
);
}
if (input.createdAt) {
result.AccidentDate = this.convertToPersianDate(input.createdAt);
result.AnnouncementDate = result.AccidentDate;
result.DocReceivedDate = result.AccidentDate;
result.AccidentTime = this.getTime24Hour(input.createdAt);
}
if (input.location?.lat && input.location?.lon) {
result.AccidentLocationAddress = await this.getAddressFromCoordinates(
input.location.lat,
input.location.lon,
);
}
result.EstimateAmount = input.damageParts
? this.calculateEstimateAmount(input.damageParts)
: 0;
this.applyFanavaranDefaultFields(result);
try {
const policyId = await input.resolvePolicyId();
if (policyId !== undefined && policyId !== null) {
result.PolicyId = policyId;
}
} catch (error) {
this.logger.error(
`${input.logPrefix} Failed to get PolicyId from external API:`,
error,
);
}
return result;
}
/**
* Fanavaran Submit - Map data from claim-request-management and request-management to third-party-car-financial-claims format
*/
@@ -3444,11 +3622,16 @@ export class ClaimRequestManagementService {
/**
* Step 1: Get appToken from GetAppToken API
*/
private async getAppToken(): Promise<string> {
private async getAppToken(
config: {
appName: string;
secret: string;
} = this.getTejaratnoFanavaranConfig(),
): Promise<string> {
try {
const requestHeaders: any = {
appname: this.APP_NAME,
secret: this.SECRET,
appname: config.appName,
secret: config.secret,
"Content-Length": "0",
};
delete requestHeaders["Content-Type"];
@@ -3500,12 +3683,18 @@ export class ClaimRequestManagementService {
/**
* Step 2: Login to get authenticationToken
*/
private async login(appToken: string): Promise<string> {
private async login(
appToken: string,
config: {
username: string;
password: string;
} = this.getTejaratnoFanavaranConfig(),
): Promise<string> {
try {
const requestHeaders: any = {
appToken: appToken,
userName: this.USERNAME,
password: this.PASSWORD,
userName: config.username,
password: config.password,
"Content-Length": "0",
};
delete requestHeaders["Content-Type"];
@@ -3559,6 +3748,65 @@ export class ClaimRequestManagementService {
}
}
private async getPolicyIdFromNationalCode(
nationalCodeOfInsurer: string,
config: {
appName: string;
secret: string;
username: string;
password: string;
corpId: string;
contractId: string;
location: string;
},
logPrefix: string,
): Promise<number | null> {
try {
const appToken = await this.getAppToken(config);
const authenticationToken = await this.login(appToken, config);
const policyInquiryUrl = `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/common/Policies/inquiry-my-policies?InsuranceLineId=5&NationalCode=${nationalCodeOfInsurer}`;
this.logger.log(
`${logPrefix} Calling policy inquiry API for nationalCode: ${nationalCodeOfInsurer}`,
);
const response = await firstValueFrom(
this.httpService.get(policyInquiryUrl, {
headers: {
authenticationToken: authenticationToken,
CorpId: config.corpId,
ContractId: config.contractId,
Location: config.location,
"Content-Type": "application/json",
},
timeout: 15000,
}),
);
if (Array.isArray(response.data) && response.data.length > 0) {
const lastPolicy = response.data[response.data.length - 1];
const policyId = lastPolicy?.PolicyId;
if (policyId !== undefined && policyId !== null) {
this.logger.log(
`${logPrefix} Successfully retrieved PolicyId from last policy entry: ${policyId}`,
);
return policyId;
}
}
this.logger.warn(`${logPrefix} No PolicyId found in API response`);
return null;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "unknown policy inquiry error";
this.logger.warn(
`${logPrefix} Policy inquiry failed; continuing with empty PolicyId: ${errorMessage}`,
);
return null;
}
}
/**
* Get PolicyId from external API
*/
@@ -3606,63 +3854,422 @@ export class ClaimRequestManagementService {
return null;
}
// Get authenticationToken
const appToken = await this.getAppToken();
const authenticationToken = await this.login(appToken);
// Call external API to get PolicyId
const policyInquiryUrl = `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/common/Policies/inquiry-my-policies?InsuranceLineId=5&NationalCode=${nationalCodeOfInsurer}`;
this.logger.log(
`[Fanavaran Submit] Calling policy inquiry API for nationalCode: ${nationalCodeOfInsurer}`,
return await this.getPolicyIdFromNationalCode(
nationalCodeOfInsurer,
this.getTejaratnoFanavaranConfig(),
`[Fanavaran Submit] claimRequestId=${claimRequestId}`,
);
const response = await firstValueFrom(
this.httpService.get(policyInquiryUrl, {
headers: {
authenticationToken: authenticationToken,
CorpId: this.CORP_ID,
ContractId: this.CONTRACT_ID,
Location: this.LOCATION,
"Content-Type": "application/json",
},
}),
);
if (
Array.isArray(response.data) &&
response.data.length > 0 &&
response.data[0].PolicyId
) {
const policyId = response.data[0].PolicyId;
this.logger.log(
`[Fanavaran Submit] Successfully retrieved PolicyId: ${policyId}`,
);
return policyId;
} else {
this.logger.warn(
`[Fanavaran Submit] No PolicyId found in API response`,
);
return null;
}
} catch (error) {
this.logger.error(
`[Fanavaran Submit] Error getting PolicyId from external API:`,
error,
);
if (isAxiosError(error)) {
const errorMessage =
error.response?.data?.Message ||
error.response?.data?.message ||
error.response?.data ||
error.message ||
"Failed to get PolicyId from external API";
this.logger.error(`[Fanavaran Submit] Error message: ${errorMessage}`);
return null;
}
}
private getNationalCodeOfInsurerForGuiltyPartyV2(
parties: Array<{
role?: PartyRole;
person?: { userId?: Types.ObjectId; nationalCodeOfInsurer?: string };
statement?: { admitsGuilt?: boolean };
}>,
guiltyPartyId?: Types.ObjectId | string | null,
): string | null {
if (!guiltyPartyId) {
return null;
}
const guiltyParty = parties.find(
(party) => party.person?.userId?.toString() === guiltyPartyId.toString(),
);
return guiltyParty?.person?.nationalCodeOfInsurer ?? null;
}
private resolveGuiltyPartyIdV2(
parties: Array<{
role?: PartyRole;
person?: { userId?: Types.ObjectId };
statement?: { admitsGuilt?: boolean };
}>,
decisionGuiltyPartyId?: Types.ObjectId | string | null,
): Types.ObjectId | string | null {
if (decisionGuiltyPartyId) {
return decisionGuiltyPartyId;
}
const admitsGuiltParty = parties.find(
(party) => party.statement?.admitsGuilt && party.person?.userId,
);
if (admitsGuiltParty?.person?.userId) {
return admitsGuiltParty.person.userId;
}
// Legacy-compatible safety fallback: when no explicit guilty marker exists,
// use FIRST party if available (same heuristic used by older flows).
const firstParty = parties.find(
(party) => party.role === PartyRole.FIRST && party.person?.userId,
);
return firstParty?.person?.userId ?? null;
}
/**
* Fanavaran Submit (Parsian / V2) - Map data from claimCases + blameCases
*/
async fanavaranSubmitParsianV2(
claimCaseId: string,
options?: { debug?: boolean },
): Promise<any> {
try {
const debug = {
claimCaseId,
steps: {
claimCaseFound: false,
blameRequestLinked: false,
blameCaseFound: false,
expertDecisionFound: false,
accidentReasonFound: false,
firstPartyFound: false,
firstPartyLocationFound: false,
damageReplyFound: false,
guiltyPartyIdFound: false,
nationalCodeOfInsurerFound: false,
policyInquiryAttempted: false,
policyInquirySucceeded: false,
accidentReasonFallbackFromSnapshot: false,
guiltyPartyFallbackUsed: false,
},
values: {
blameRequestId: null as string | null,
guiltyPartyId: null as string | null,
nationalCodeOfInsurer: null as string | null,
policyId: null as number | null,
estimateAmount: null as number | null,
},
failureReason: null as string | null,
};
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
if (!claimCase) {
throw new NotFoundException("Claim case not found");
}
debug.steps.claimCaseFound = true;
if (!claimCase.blameRequestId) {
debug.failureReason = "claimCase.blameRequestId is missing";
throw new BadRequestException("Blame case not linked to claim case");
}
debug.steps.blameRequestLinked = true;
debug.values.blameRequestId = claimCase.blameRequestId.toString();
const blameCase = await this.blameRequestDbService.findById(
claimCase.blameRequestId,
);
if (!blameCase) {
throw new NotFoundException("Blame case not found");
}
debug.steps.blameCaseFound = true;
const expertDecision = blameCase.expert?.decision;
debug.steps.expertDecisionFound = !!expertDecision;
const fallbackAccidentReason = claimCase.snapshot?.accident?.classification?.accidentReason;
if (!expertDecision?.fields?.accidentReason && fallbackAccidentReason) {
debug.steps.accidentReasonFallbackFromSnapshot = true;
}
const selectedAccidentReason =
expertDecision?.fields?.accidentReason ?? fallbackAccidentReason;
debug.steps.accidentReasonFound = !!selectedAccidentReason;
const firstParty = blameCase.parties?.find(
(party) => party.role === PartyRole.FIRST,
);
debug.steps.firstPartyFound = !!firstParty;
debug.steps.firstPartyLocationFound = !!firstParty?.location;
const activeExpertReply = getActiveV2ExpertReply(
claimCase as unknown as { evaluation?: Record<string, unknown> },
);
const damageParts = activeExpertReply?.parts;
debug.steps.damageReplyFound = !!damageParts?.length;
if (options?.debug) {
debug.values.estimateAmount = damageParts
? this.calculateEstimateAmount(damageParts)
: 0;
}
const payload = await this.buildFanavaranSubmitPayload({
accidentReason: selectedAccidentReason,
createdAt: (blameCase as { createdAt?: Date }).createdAt,
location: firstParty?.location,
damageParts,
defaults: this.PARSIAN_FANAVARAN_DEFAULTS,
logPrefix: `[Fanavaran Parsian V2] claimCaseId=${claimCaseId}`,
resolvePolicyId: async () => {
const guiltyPartyId = this.resolveGuiltyPartyIdV2(
blameCase.parties ?? [],
expertDecision?.guiltyPartyId,
);
if (!expertDecision?.guiltyPartyId && guiltyPartyId) {
debug.steps.guiltyPartyFallbackUsed = true;
}
debug.steps.guiltyPartyIdFound = !!guiltyPartyId;
debug.values.guiltyPartyId = guiltyPartyId
? guiltyPartyId.toString()
: null;
const nationalCodeOfInsurer = this.getNationalCodeOfInsurerForGuiltyPartyV2(
blameCase.parties ?? [],
guiltyPartyId,
);
debug.steps.nationalCodeOfInsurerFound = !!nationalCodeOfInsurer;
debug.values.nationalCodeOfInsurer = nationalCodeOfInsurer;
if (!guiltyPartyId) {
this.logger.warn(
`[Fanavaran Parsian V2] guiltyPartyId not found in expert.decision`,
);
debug.failureReason = "expert.decision.guiltyPartyId is missing";
return null;
}
if (!nationalCodeOfInsurer) {
this.logger.warn(
`[Fanavaran Parsian V2] nationalCodeOfInsurer not found for guiltyPartyId: ${guiltyPartyId}`,
);
debug.failureReason =
"nationalCodeOfInsurer not found for guilty party in blameCase.parties";
return null;
}
debug.steps.policyInquiryAttempted = true;
const policyId = await this.getPolicyIdFromNationalCode(
nationalCodeOfInsurer,
this.PARSIAN_FANAVARAN_CONFIG,
`[Fanavaran Parsian V2] claimCaseId=${claimCaseId}`,
);
debug.values.policyId = policyId;
debug.steps.policyInquirySucceeded = policyId !== null;
if (policyId === null) {
debug.failureReason =
debug.failureReason ??
"policy inquiry returned no PolicyId (timeout, network error, or empty response)";
}
return policyId;
},
});
if (options?.debug) {
return {
payload,
debug,
};
}
return payload;
} catch (error) {
this.logger.error("Error in fanavaranSubmitParsianV2", error);
throw error;
}
}
/**
* Submit Parsian fanavaran data (V2 collections) to external API
*/
async submitToFanavaranParsianV2(claimCaseId: string): Promise<any> {
try {
return await this.executeFanavaranParsianV2Submit(claimCaseId);
} catch (error) {
this.logger.error(
`[Fanavaran Parsian V2] Error submitting to Fanavaran`,
error,
);
throw new BadGatewayException(this.extractFanavaranErrorMessage(error));
}
}
/**
* Auto-submit to Fanavaran when a claim case reaches COMPLETED.
* Never throws — failures are returned as warnings so the main flow continues.
*/
async autoSubmitToFanavaranParsianV2OnClaimCompleted(
claimCaseId: string,
): Promise<FanavaranAutoSubmitResult> {
const logPrefix = `[Fanavaran Parsian V2 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 auto-submit. Retry manually when available.",
};
}
if (claimCase.claimId != null || claimCase.claimNo != null) {
return {
attempted: false,
submitted: false,
skipped: true,
skipReason: "Already submitted to Fanavaran",
claimId: claimCase.claimId,
claimNo: claimCase.claimNo,
};
}
const fanavaranResponse = await this.executeFanavaranParsianV2Submit(
claimCaseId,
);
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
$push: {
history: {
type: "FANAVARAN_AUTO_SUBMIT_SUCCEEDED",
actor: { actorType: "system" },
timestamp: new Date(),
metadata: {
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} Auto-submit failed: ${warning}`);
try {
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
$push: {
history: {
type: "FANAVARAN_AUTO_SUBMIT_FAILED",
actor: { actorType: "system" },
timestamp: new Date(),
metadata: { error: warning },
},
},
});
} catch (historyError) {
this.logger.error(
`${logPrefix} Failed to record Fanavaran auto-submit failure history`,
historyError,
);
}
return {
attempted: true,
submitted: false,
warning: `${warning} Case was not sent to Fanavaran. Retry manually via POST /v2/claim-request-management/fanavaran-submit/parsian/${claimCaseId}.`,
};
}
}
private extractFanavaranErrorMessage(error: unknown): string {
if (isAxiosError(error)) {
return (
error.response?.data?.message ||
error.response?.data?.Message ||
(typeof error.response?.data === "string"
? error.response.data
: undefined) ||
error.message ||
"Failed to submit data to Fanavaran API"
);
}
if (error instanceof HttpException) {
const response = error.getResponse();
if (typeof response === "string") return response;
if (response && typeof response === "object" && "message" in response) {
const message = (response as { message?: string | string[] }).message;
return Array.isArray(message) ? message.join(", ") : String(message);
}
}
if (error instanceof Error) {
return error.message;
}
return "Failed to submit data to Fanavaran API";
}
private async executeFanavaranParsianV2Submit(
claimCaseId: string,
): Promise<any> {
const config = this.PARSIAN_FANAVARAN_CONFIG;
const logPrefix = `[Fanavaran Parsian V2]`;
this.logger.log(
`${logPrefix} Starting submission for claimCaseId: ${claimCaseId}`,
);
const fanavaranData = await this.fanavaranSubmitParsianV2(claimCaseId);
this.logger.log(
`${logPrefix} Mapped data prepared:`,
JSON.stringify(fanavaranData, null, 2),
);
if (
fanavaranData.AccidentCauseId === undefined ||
fanavaranData.AccidentCauseId === null
) {
this.logger.error(
`${logPrefix} CRITICAL: AccidentCauseId is missing or null!`,
);
} else {
this.logger.log(
`${logPrefix} AccidentCauseId validated: ${fanavaranData.AccidentCauseId}`,
);
}
const appToken = await this.getAppToken(config);
const authenticationToken = await this.login(appToken, config);
const response = await firstValueFrom(
this.httpService.post(this.FANAVARAN_SUBMIT_URL, fanavaranData, {
headers: {
authenticationToken: authenticationToken,
CorpId: config.corpId,
ContractId: config.contractId,
Location: config.location,
"Content-Type": "application/json",
},
}),
);
this.logger.log(`${logPrefix} API Response Status: ${response.status}`);
this.logger.log(
`${logPrefix} API Response Data:`,
JSON.stringify(response.data, null, 2),
);
if (response.data) {
const claimNo = response.data.ClaimNo;
const claimId = response.data.Id;
if (claimNo !== undefined || claimId !== undefined) {
const updateData: any = { $set: {} as Record<string, unknown> };
if (claimNo !== undefined) {
updateData.$set.claimNo = claimNo;
}
if (claimId !== undefined) {
updateData.$set.claimId = claimId;
}
await this.claimCaseDbService.findByIdAndUpdate(
claimCaseId,
updateData,
);
}
}
return response.data;
}
/**
* Submit fanavaran data to external API
*/
@@ -6306,6 +6913,7 @@ export class ClaimRequestManagementService {
currentStep?: ClaimWorkflowStep;
accepted: boolean;
phase?: "PRICED_PARTS_FOR_FACTORS" | "FINAL_APPROVAL";
fanavaran?: FanavaranAutoSubmitResult;
}> {
if (!Types.ObjectId.isValid(claimRequestId)) {
throw new BadRequestException("Invalid claim request id");
@@ -6558,14 +7166,26 @@ export class ClaimRequestManagementService {
},
});
const fanavaran =
await this.autoSubmitToFanavaranParsianV2OnClaimCompleted(claimRequestId);
let message = "Your signature has been recorded. The claim is completed.";
if (fanavaran.submitted) {
message +=
" The claim was sent to Fanavaran successfully.";
} else if (fanavaran.warning) {
message += ` ${fanavaran.warning}`;
}
return {
message: "Your signature has been recorded. The claim is completed.",
message,
claimRequestId,
status: ClaimCaseStatus.COMPLETED,
claimStatus: ClaimStatus.APPROVED,
currentStep: ClaimWorkflowStep.CLAIM_COMPLETED,
accepted: true,
phase: "FINAL_APPROVAL",
fanavaran,
};
}

View File

@@ -1029,4 +1029,44 @@ Returns status of each item (uploaded/captured or not).
);
}
}
@Get("fanavaran-submit/parsian/:claimCaseId")
@ApiOperation({
summary: "Preview Fanavaran submit body (Parsian / V2)",
description:
"Builds the third-party-car-financial-claims payload from claimCases + blameCases without calling Fanavaran.",
})
@ApiParam({
name: "claimCaseId",
description: "The claim case ID (MongoDB ObjectId)",
})
async fanavaranSubmitParsianV2(
@Param("claimCaseId") claimCaseId: string,
@Query("debug") debug?: string,
) {
return await this.claimRequestManagementService.fanavaranSubmitParsianV2(
claimCaseId,
{
debug: debug === "1" || debug === "true",
},
);
}
@Post("fanavaran-submit/parsian/:claimCaseId")
@ApiOperation({
summary: "Submit claim to Fanavaran (Parsian / V2)",
description:
"Authenticates with Parsian Fanavaran credentials and submits the mapped claimCases + blameCases payload.",
})
@ApiParam({
name: "claimCaseId",
description: "The claim case ID (MongoDB ObjectId)",
})
async submitToFanavaranParsianV2(
@Param("claimCaseId") claimCaseId: string,
) {
return await this.claimRequestManagementService.submitToFanavaranParsianV2(
claimCaseId,
);
}
}