forked from Yara724/api
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:
@@ -151,6 +151,17 @@ import { HttpService } from "@nestjs/axios";
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
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()
|
@Injectable()
|
||||||
export class ClaimRequestManagementService {
|
export class ClaimRequestManagementService {
|
||||||
private readonly logger = new Logger(ClaimRequestManagementService.name);
|
private readonly logger = new Logger(ClaimRequestManagementService.name);
|
||||||
@@ -174,6 +185,41 @@ export class ClaimRequestManagementService {
|
|||||||
private readonly CONTRACT_ID = "263";
|
private readonly CONTRACT_ID = "263";
|
||||||
private readonly LOCATION = "100";
|
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(
|
constructor(
|
||||||
private readonly blameDbService: RequestManagementDbService,
|
private readonly blameDbService: RequestManagementDbService,
|
||||||
private readonly claimDbService: ClaimRequestManagementDbService,
|
private readonly claimDbService: ClaimRequestManagementDbService,
|
||||||
@@ -3170,15 +3216,13 @@ export class ClaimRequestManagementService {
|
|||||||
lon: number,
|
lon: number,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const apiKey =
|
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2In0.eyJhdWQiOiIyMTcxOCIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2IiwiaWF0IjoxNjgwNjA4NTkxLCJuYmYiOjE2ODA2MDg1OTEsImV4cCI6MTY4MzIwMDU5MSwic3ViIjoiIiwic2NvcGVzIjpbImJhc2ljIl19.rTviLd8b5yTHUDa3ODZyva593eMnL0d3XPg3sKkZxMOf_jNIH6lFQyIfbId-wsd1EAdsOdsL3CME_Y8t332PWJbxMNgnEq4Rf2IkClkvkSx6Sb5_4bmlhBM75zw2SmccvgbFUn4xkTOw0FT4vABC2Y3-MKctjMpmO8QOrVULSKt4psrmQhr7hBu7YRDnAAEc6muZ1VpRvdB1kqNKddoSIrfDaq6aDRJ-BNbGRAaFFvP_kH4cgSCKV4dU0TknL3mRKUiVy6_TDkjtzAN8fE2wsdvNo2pGTJPzKFsR2ipgGNTvB__g3bOnVpKsgFXPBH0e_Qa7ff1tZ3VGWy3jRNh9Lg";
|
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.get(
|
this.httpService.get(
|
||||||
`https://map.ir/fast-reverse?lat=${lat}&lon=${lon}`,
|
`https://map.ir/fast-reverse?lat=${lat}&lon=${lon}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
"x-api-key": apiKey,
|
"x-api-key": this.MAP_IR_API_KEY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -3226,6 +3270,140 @@ export class ClaimRequestManagementService {
|
|||||||
return ids;
|
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
|
* 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
|
* 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 {
|
try {
|
||||||
const requestHeaders: any = {
|
const requestHeaders: any = {
|
||||||
appname: this.APP_NAME,
|
appname: config.appName,
|
||||||
secret: this.SECRET,
|
secret: config.secret,
|
||||||
"Content-Length": "0",
|
"Content-Length": "0",
|
||||||
};
|
};
|
||||||
delete requestHeaders["Content-Type"];
|
delete requestHeaders["Content-Type"];
|
||||||
@@ -3500,12 +3683,18 @@ export class ClaimRequestManagementService {
|
|||||||
/**
|
/**
|
||||||
* Step 2: Login to get authenticationToken
|
* 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 {
|
try {
|
||||||
const requestHeaders: any = {
|
const requestHeaders: any = {
|
||||||
appToken: appToken,
|
appToken: appToken,
|
||||||
userName: this.USERNAME,
|
userName: config.username,
|
||||||
password: this.PASSWORD,
|
password: config.password,
|
||||||
"Content-Length": "0",
|
"Content-Length": "0",
|
||||||
};
|
};
|
||||||
delete requestHeaders["Content-Type"];
|
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
|
* Get PolicyId from external API
|
||||||
*/
|
*/
|
||||||
@@ -3606,63 +3854,422 @@ export class ClaimRequestManagementService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get authenticationToken
|
return await this.getPolicyIdFromNationalCode(
|
||||||
const appToken = await this.getAppToken();
|
nationalCodeOfInsurer,
|
||||||
const authenticationToken = await this.login(appToken);
|
this.getTejaratnoFanavaranConfig(),
|
||||||
|
`[Fanavaran Submit] claimRequestId=${claimRequestId}`,
|
||||||
// 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}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[Fanavaran Submit] Error getting PolicyId from external API:`,
|
`[Fanavaran Submit] Error getting PolicyId from external API:`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
if (isAxiosError(error)) {
|
return null;
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
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
|
* Submit fanavaran data to external API
|
||||||
*/
|
*/
|
||||||
@@ -6306,6 +6913,7 @@ export class ClaimRequestManagementService {
|
|||||||
currentStep?: ClaimWorkflowStep;
|
currentStep?: ClaimWorkflowStep;
|
||||||
accepted: boolean;
|
accepted: boolean;
|
||||||
phase?: "PRICED_PARTS_FOR_FACTORS" | "FINAL_APPROVAL";
|
phase?: "PRICED_PARTS_FOR_FACTORS" | "FINAL_APPROVAL";
|
||||||
|
fanavaran?: FanavaranAutoSubmitResult;
|
||||||
}> {
|
}> {
|
||||||
if (!Types.ObjectId.isValid(claimRequestId)) {
|
if (!Types.ObjectId.isValid(claimRequestId)) {
|
||||||
throw new BadRequestException("Invalid claim request id");
|
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 {
|
return {
|
||||||
message: "Your signature has been recorded. The claim is completed.",
|
message,
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
status: ClaimCaseStatus.COMPLETED,
|
status: ClaimCaseStatus.COMPLETED,
|
||||||
claimStatus: ClaimStatus.APPROVED,
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
currentStep: ClaimWorkflowStep.CLAIM_COMPLETED,
|
currentStep: ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
phase: "FINAL_APPROVAL",
|
phase: "FINAL_APPROVAL",
|
||||||
|
fanavaran,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,27 @@ export const REPAIR_LINE_AMOUNT_TOMAN = {
|
|||||||
|
|
||||||
/** Max sum of all priced + factor lines in one expert reply / validation (Toman). */
|
/** Max sum of all priced + factor lines in one expert reply / validation (Toman). */
|
||||||
export const CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN = REPAIR_LINE_AMOUNT_TOMAN.MAX;
|
export const CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN = REPAIR_LINE_AMOUNT_TOMAN.MAX;
|
||||||
|
|
||||||
|
const ENABLED_VALUES = new Set(["1", "true", "yes", "on", "enabled"]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns null when the claim v2 total cap is disabled.
|
||||||
|
*
|
||||||
|
* Set CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED=true to enforce the cap again.
|
||||||
|
* Optionally set CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN to override the amount.
|
||||||
|
*/
|
||||||
|
export function getClaimV2TotalPaymentCapToman(): number | null {
|
||||||
|
const capEnabled = ENABLED_VALUES.has(
|
||||||
|
String(process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!capEnabled) return null;
|
||||||
|
|
||||||
|
const configuredCap = Number(process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN);
|
||||||
|
|
||||||
|
return Number.isFinite(configuredCap) && configuredCap > 0
|
||||||
|
? configuredCap
|
||||||
|
: CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN;
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,4 +84,26 @@ export class EnvironmentVariables {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
HASH_PEPPER?: string;
|
HASH_PEPPER?: string;
|
||||||
// --------------------------------------------------------- //
|
// --------------------------------------------------------- //
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: "CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED must be string" })
|
||||||
|
CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED?: string;
|
||||||
|
|
||||||
|
// --------------------------------------------------------- //
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber(
|
||||||
|
{
|
||||||
|
allowNaN: false,
|
||||||
|
allowInfinity: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN must be a valid number",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@IsPositive({
|
||||||
|
message: "CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN must be a positive number",
|
||||||
|
})
|
||||||
|
CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN?: number;
|
||||||
|
// --------------------------------------------------------- //
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import { distance as stringDistance } from "fastest-levenshtein"; // حتما ن
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { lastValueFrom } from "rxjs";
|
import { lastValueFrom } from "rxjs";
|
||||||
import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service";
|
import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service";
|
||||||
|
import {
|
||||||
|
ClaimRequestManagementService,
|
||||||
|
FanavaranAutoSubmitResult,
|
||||||
|
} from "src/claim-request-management/claim-request-management.service";
|
||||||
import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.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";
|
import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service";
|
||||||
import { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.db.service";
|
import { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.db.service";
|
||||||
@@ -135,14 +139,12 @@ import {
|
|||||||
} from "src/users/entities/schema/expert-file-activity.schema";
|
} from "src/users/entities/schema/expert-file-activity.schema";
|
||||||
|
|
||||||
/** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */
|
/** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */
|
||||||
import { CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN } from "src/constants/repair-amount-limits";
|
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
|
||||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
||||||
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
|
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
|
||||||
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
|
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
|
||||||
|
|
||||||
const CLAIM_V2_TOTAL_PAYMENT_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpertClaimService {
|
export class ExpertClaimService {
|
||||||
private readonly logger = new Logger(ExpertClaimService.name);
|
private readonly logger = new Logger(ExpertClaimService.name);
|
||||||
@@ -268,8 +270,22 @@ export class ExpertClaimService {
|
|||||||
private readonly userDbService: UserDbService,
|
private readonly userDbService: UserDbService,
|
||||||
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
|
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
|
||||||
private readonly claimSignDbService: ClaimSignDbService,
|
private readonly claimSignDbService: ClaimSignDbService,
|
||||||
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private appendFanavaranAutoSubmitToMessage(
|
||||||
|
baseMessage: string,
|
||||||
|
fanavaran: FanavaranAutoSubmitResult,
|
||||||
|
): string {
|
||||||
|
if (fanavaran.submitted) {
|
||||||
|
return `${baseMessage} The claim was sent to Fanavaran successfully.`;
|
||||||
|
}
|
||||||
|
if (fanavaran.warning) {
|
||||||
|
return `${baseMessage} ${fanavaran.warning}`;
|
||||||
|
}
|
||||||
|
return baseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a `claim-sign` document id to a downloadable file URL.
|
* Resolve a `claim-sign` document id to a downloadable file URL.
|
||||||
* Mirrors the helper used in `ExpertInsurerService` so signature links are
|
* Mirrors the helper used in `ExpertInsurerService` so signature links are
|
||||||
@@ -1561,9 +1577,9 @@ export class ExpertClaimService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate total price cap (priced lines sum)
|
// Validate total price cap (priced lines sum), when enabled.
|
||||||
if (reply.parts && reply.parts.length > 0) {
|
const priceCap = getClaimV2TotalPaymentCapToman();
|
||||||
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
|
if (priceCap !== null && reply.parts && reply.parts.length > 0) {
|
||||||
let totalPrice = 0;
|
let totalPrice = 0;
|
||||||
|
|
||||||
for (const part of reply.parts) {
|
for (const part of reply.parts) {
|
||||||
@@ -1592,13 +1608,13 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalPrice > PRICE_CAP) {
|
if (totalPrice > priceCap) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
|
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
|
||||||
error: "PRICE_CAP_ERROR",
|
error: "PRICE_CAP_ERROR",
|
||||||
code: "PRICE_CAP_ERROR",
|
code: "PRICE_CAP_ERROR",
|
||||||
totalPrice: totalPrice,
|
totalPrice: totalPrice,
|
||||||
priceCap: PRICE_CAP,
|
priceCap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2160,7 +2176,7 @@ export class ExpertClaimService {
|
|||||||
* Preconditions: all `factorNeeded` parts have `factorLink`; case is UNDER_REVIEW at EXPERT_COST_EVALUATION.
|
* Preconditions: all `factorNeeded` parts have `factorLink`; case is UNDER_REVIEW at EXPERT_COST_EVALUATION.
|
||||||
* — All approved → COMPLETED + APPROVED (expert-entered line totals; no extra owner signature).
|
* — All approved → COMPLETED + APPROVED (expert-entered line totals; no extra owner signature).
|
||||||
* — Any rejected (repriced) → COMPLETED + APPROVED (auto-close for now; owner sign may be added later).
|
* — Any rejected (repriced) → COMPLETED + APPROVED (auto-close for now; owner sign may be added later).
|
||||||
* Total of all repair lines must be ≤ `CLAIM_V2_TOTAL_PAYMENT_CAP` Toman (53_000_000; same as initial expert reply).
|
* When enabled by env, total of all repair lines must be ≤ the claim v2 total payment cap.
|
||||||
* Response: `claimStatus` = `ClaimStatus`; `caseStatus` = `ClaimCaseStatus`.
|
* Response: `claimStatus` = `ClaimStatus`; `caseStatus` = `ClaimCaseStatus`.
|
||||||
*/
|
*/
|
||||||
async validateClaimFactorsV2(
|
async validateClaimFactorsV2(
|
||||||
@@ -2295,7 +2311,8 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
|
const priceCap = getClaimV2TotalPaymentCapToman();
|
||||||
|
if (priceCap !== null) {
|
||||||
let totalPrice = 0;
|
let totalPrice = 0;
|
||||||
for (const part of updatedReply.parts || []) {
|
for (const part of updatedReply.parts || []) {
|
||||||
const line = this.claimReplyPartLineTotalForCap(part);
|
const line = this.claimReplyPartLineTotalForCap(part);
|
||||||
@@ -2307,14 +2324,15 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
totalPrice += line;
|
totalPrice += line;
|
||||||
}
|
}
|
||||||
if (totalPrice > PRICE_CAP) {
|
if (totalPrice > priceCap) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
|
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
|
||||||
error: "PRICE_CAP_ERROR",
|
error: "PRICE_CAP_ERROR",
|
||||||
totalPrice,
|
totalPrice,
|
||||||
priceCap: PRICE_CAP,
|
priceCap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const historyActor = {
|
const historyActor = {
|
||||||
actorId: new Types.ObjectId(actor.sub),
|
actorId: new Types.ObjectId(actor.sub),
|
||||||
@@ -2331,13 +2349,20 @@ export class ExpertClaimService {
|
|||||||
historyActor,
|
historyActor,
|
||||||
{ replyField },
|
{ replyField },
|
||||||
);
|
);
|
||||||
|
const fanavaran =
|
||||||
|
await this.claimRequestManagementService.autoSubmitToFanavaranParsianV2OnClaimCompleted(
|
||||||
|
claimRequestId,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
message:
|
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).",
|
"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,
|
||||||
|
),
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
claimStatus: ClaimStatus.APPROVED,
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
caseStatus: ClaimCaseStatus.COMPLETED,
|
caseStatus: ClaimCaseStatus.COMPLETED,
|
||||||
outcome: "REJECTED_REPRICED_AUTO_COMPLETED",
|
outcome: "REJECTED_REPRICED_AUTO_COMPLETED",
|
||||||
|
fanavaran,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2350,13 +2375,21 @@ export class ExpertClaimService {
|
|||||||
{ replyField },
|
{ replyField },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fanavaran =
|
||||||
|
await this.claimRequestManagementService.autoSubmitToFanavaranParsianV2OnClaimCompleted(
|
||||||
|
claimRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message:
|
message: this.appendFanavaranAutoSubmitToMessage(
|
||||||
"All factors were approved by the expert. The claim is completed without an additional owner signature.",
|
"All factors were approved by the expert. The claim is completed without an additional owner signature.",
|
||||||
|
fanavaran,
|
||||||
|
),
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
claimStatus: ClaimStatus.APPROVED,
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
caseStatus: ClaimCaseStatus.COMPLETED,
|
caseStatus: ClaimCaseStatus.COMPLETED,
|
||||||
outcome: "ALL_APPROVED_AUTO_COMPLETED",
|
outcome: "ALL_APPROVED_AUTO_COMPLETED",
|
||||||
|
fanavaran,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2950,21 +2983,23 @@ export class ExpertClaimService {
|
|||||||
throw new ForbiddenException("This claim is locked by another expert");
|
throw new ForbiddenException("This claim is locked by another expert");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Price cap validation
|
// Price cap validation, when enabled.
|
||||||
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
|
const priceCap = getClaimV2TotalPaymentCapToman();
|
||||||
|
if (priceCap !== null) {
|
||||||
let totalPrice = 0;
|
let totalPrice = 0;
|
||||||
for (const part of reply.parts || []) {
|
for (const part of reply.parts || []) {
|
||||||
const parsed = this.parsePersianNumber(String(part.totalPayment ?? "0"));
|
const parsed = this.parsePersianNumber(String(part.totalPayment ?? "0"));
|
||||||
if (!isNaN(parsed)) totalPrice += parsed;
|
if (!isNaN(parsed)) totalPrice += parsed;
|
||||||
}
|
}
|
||||||
if (totalPrice > PRICE_CAP) {
|
if (totalPrice > priceCap) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
|
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
|
||||||
error: "PRICE_CAP_ERROR",
|
error: "PRICE_CAP_ERROR",
|
||||||
totalPrice,
|
totalPrice,
|
||||||
priceCap: PRICE_CAP,
|
priceCap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const carTypeSubmit = claim.vehicle?.carType as
|
const carTypeSubmit = claim.vehicle?.carType as
|
||||||
| ClaimVehicleTypeV2
|
| ClaimVehicleTypeV2
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BadRequestException, ForbiddenException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException } from "@nestjs/common";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
|
||||||
/** Insurance company tenant id on the actor JWT (Mongo ObjectId string). */
|
/** Insurance company tenant id on the actor JWT (Mongo ObjectId string). */
|
||||||
export function requireActorClientKey(actor: { clientKey?: string }): string {
|
export function requireActorClientKey(actor: { clientKey?: string }): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user