Merge pull request 'mapping problems fixed' (#237) from s.hajizadeh/yara724api:main into main

Reviewed-on: Yara724/api#237
This commit is contained in:
2026-08-01 17:57:41 +03:30
5 changed files with 283 additions and 78 deletions

View File

@@ -809,7 +809,12 @@ export class ClaimRequestManagementService {
throw er;
}
if (er.code === 11000) {
if (
typeof er === "object" &&
er !== null &&
"code" in er &&
er.code === 11000
) {
throw new HttpException(
"Duplicate claim file reference.",
HttpStatus.CONFLICT,
@@ -1413,8 +1418,8 @@ export class ClaimRequestManagementService {
return new ClaimPartUploadDetail([updatedClaim]);
} catch (error) {
this.logger.error(
`Error in selectCarOtherPartDamage: ${error.message}`,
error.stack,
`Error in selectCarOtherPartDamage: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
// Re-throw the specific error from the validation steps or a generic one.
throw error;
@@ -1516,7 +1521,7 @@ export class ClaimRequestManagementService {
}
} catch (fileError) {
this.logger.warn(
`Failed to delete file ${existingDoc.path}: ${fileError.message}`,
`Failed to delete file ${existingDoc.path}: ${fileError instanceof Error ? fileError.message : String(fileError)}`,
);
// Continue even if file deletion fails
}
@@ -1612,8 +1617,8 @@ export class ClaimRequestManagementService {
};
} catch (error) {
this.logger.error(
`Error in uploadRequiredDocument: ${error.message}`,
error.stack,
`Error in uploadRequiredDocument: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
throw error;
}
@@ -1658,8 +1663,8 @@ export class ClaimRequestManagementService {
};
} catch (error) {
this.logger.error(
`Error in getRequiredDocumentsStatus: ${error.message}`,
error.stack,
`Error in getRequiredDocumentsStatus: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
throw error;
}
@@ -1922,8 +1927,8 @@ export class ClaimRequestManagementService {
);
} catch (error) {
this.logger.error(
`[BACKGROUND AI] Failed to process AI requests for part ${partId}, request ${requestId}: ${error.message}`,
error.stack,
`[BACKGROUND AI] Failed to process AI requests for part ${partId}, request ${requestId}: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
// Don't throw - we're in background processing
}
@@ -2013,11 +2018,15 @@ export class ClaimRequestManagementService {
`[SUCCESS] Successfully updated AI report and image for part ${partId}`,
);
} catch (dbError) {
const dbErrorMessage =
dbError instanceof Error ? dbError.message : String(dbError);
this.logger.error(`[ERROR] Database update error for part ${partId}:`);
this.logger.error(`[ERROR] ${dbError.message}`);
this.logger.error(`[ERROR] Stack: ${dbError.stack}`);
this.logger.error(`[ERROR] ${dbErrorMessage}`);
this.logger.error(
`[ERROR] Stack: ${dbError instanceof Error ? dbError.stack : undefined}`,
);
throw new HttpException(
`Database update failed: ${dbError.message}`,
`Database update failed: ${dbErrorMessage}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
@@ -2053,7 +2062,8 @@ export class ClaimRequestManagementService {
);
} catch (error) {
// Extract error source from error message if available
const errorMessage = error.message || String(error);
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorSource = errorMessage.includes("[ERROR]")
? errorMessage.split("[ERROR]")[1]?.split("]")[0] || "UNKNOWN"
: "UNKNOWN";
@@ -2089,7 +2099,7 @@ export class ClaimRequestManagementService {
this.logger.error(`🔴 [FINAL FAILURE] File path: ${newImageDoc.path}`);
this.logger.error(`🔴 [FINAL FAILURE] Error source: ${errorSource}`);
this.logger.error(`🔴 [FINAL FAILURE] Final error: ${errorMessage}`);
if (error.stack) {
if (error instanceof Error && error.stack) {
this.logger.error(`🔴 [FINAL FAILURE] Stack trace: ${error.stack}`);
}
// Optional: Update the claim status to indicate an AI failure.
@@ -2856,7 +2866,7 @@ export class ClaimRequestManagementService {
} catch (error) {
this.logger.error(
`Error during factor upload for claim ${claimId}:`,
error.stack,
error instanceof Error ? error.stack : String(error),
);
if (error instanceof HttpException) {
@@ -3155,7 +3165,12 @@ export class ClaimRequestManagementService {
};
} catch (err) {
this.logger.error(err);
return new HttpException(err.message, err.status);
return new HttpException(
err instanceof HttpException ? err.message : String(err),
err instanceof HttpException
? err.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -3214,7 +3229,12 @@ export class ClaimRequestManagementService {
};
} catch (err) {
this.logger.error(err);
return new HttpException(err.message, err.status);
return new HttpException(
err instanceof HttpException ? err.message : String(err),
err instanceof HttpException
? err.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -3609,6 +3629,53 @@ export class ClaimRequestManagementService {
return `${plate.centerDigits}${plate.centerAlphabet}${plate.leftDigits}`;
}
/** First non-empty string from party vehicle.inquiry mapped/raw (ESG + Tejarat aliases). */
private pickPartyInquiryField(
mapped: Record<string, any> | null | undefined,
raw: Record<string, any> | null | undefined,
keys: string[],
): string | null {
for (const key of keys) {
const value = mapped?.[key] ?? raw?.[key];
if (value == null) continue;
const text = String(value).trim();
if (text) return text;
}
return null;
}
private resolvePlateFromPartyInquiry(
mapped: Record<string, any> | null | undefined,
raw: Record<string, any> | null | undefined,
): {
leftDigits: number;
centerAlphabet: string;
centerDigits: number;
ir: number;
} | null {
const left = this.pickPartyInquiryField(mapped, raw, ["plk1", "Plk1"]);
const middle = this.pickPartyInquiryField(mapped, raw, ["plk2", "Plk2"]);
const right = this.pickPartyInquiryField(mapped, raw, ["plk3", "Plk3"]);
const serial = this.pickPartyInquiryField(mapped, raw, [
"plksrl",
"PlkSrl",
]);
if (!left || !middle || !right || !serial) return null;
const leftDigits = Number(left);
const centerDigits = Number(right);
const ir = Number(serial);
const centerAlphabet = this.plateNormalizer.normalizePlateText(middle);
if (
!Number.isFinite(leftDigits) ||
!Number.isFinite(centerDigits) ||
!Number.isFinite(ir) ||
!centerAlphabet
) {
return null;
}
return { leftDigits, centerAlphabet, centerDigits, ir };
}
private async resolveVehicleKindId(
clientKey: FanavaranClientKey,
carType?: string,
@@ -3781,10 +3848,6 @@ export class ClaimRequestManagementService {
FaultPercent: number;
};
}): Promise<Record<string, unknown>> {
const plate = this.resolveOwnershipPlateForClaim(
input.claimCase,
input.blameCase,
);
const damagedParty = Array.isArray(input.blameCase?.parties)
? input.blameCase.parties.find(
(party: any) =>
@@ -3795,8 +3858,19 @@ export class ClaimRequestManagementService {
const person = damagedParty?.person ?? {};
const vehicle = damagedParty?.vehicle ?? {};
const insurance = damagedParty?.insurance ?? {};
const inquiryMapped = vehicle?.inquiry?.mapped ?? {};
const inquiryRaw = vehicle?.inquiry?.raw ?? {};
// Prefer nested inquiry (ESG/Tejarat) on the damaged party — not flattened insurance.*
const inquiryRawPayload =
vehicle?.inquiry?.raw?.data != null &&
typeof vehicle.inquiry.raw.data === "object" &&
!Array.isArray(vehicle.inquiry.raw.data)
? vehicle.inquiry.raw.data
: (vehicle?.inquiry?.raw ?? {});
const inquiryMapped = vehicle?.inquiry?.mapped ?? inquiryRawPayload ?? {};
const inquiryRaw = inquiryRawPayload ?? {};
const plate =
this.resolveOwnershipPlateForClaim(input.claimCase, input.blameCase) ??
this.resolvePlateFromPartyInquiry(inquiryMapped, inquiryRaw);
this.logger.log(
`[buildFanavaranDamageCasePayload] damagedParty resolved: userId=${damagedParty?.person?.userId ?? "NONE"}, ` +
@@ -3812,22 +3886,44 @@ export class ClaimRequestManagementService {
);
}
const guiltyUserId =
input.blameCase?.expertSubmitReplyFinal?.guiltyUserId ??
input.blameCase?.expertSubmitReply?.guiltyUserId;
const blamedParty =
guiltyUserId && Array.isArray(input.blameCase?.parties)
? input.blameCase.parties.find(
(party: any) =>
String(party?.person?.userId ?? "") === String(guiltyUserId),
)
: null;
// Get PolicyCINumber from damaged party's inquiry (mapped or raw), not blamed party
const policyCINumber =
inquiryMapped.ThirdPolicyCode ??
inquiryRaw.ThirdPolicyCode ??
// Policy document / unique code / coverage dates from damaged party's plate inquiry
const policyNo =
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"PrntCmpDocNo",
"PrntPlcyCmpDocNo",
"printNumber",
"insuranceNumber",
]) ??
insurance.policyNumber ??
null;
const policyCINumber = this.pickPartyInquiryField(
inquiryMapped,
inquiryRaw,
["PlcyUnqCod", "ThirdPolicyCode"],
);
const beginDate =
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"HBgnDte",
"StartDate",
"persianStartDate",
"SatrtDate",
]) ??
insurance.startDate ??
null;
const endDate =
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"HEndDte",
"EndDate",
"persianEndDate",
]) ??
insurance.endDate ??
null;
const builtYearRaw = this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"PrdDte",
"ModelField",
"ModelCii",
]);
const builtYear = builtYearRaw ? Number(builtYearRaw) || null : null;
const carType = input.claimCase?.vehicle?.carType as string | undefined;
const vehicleKindId = await this.resolveVehicleKindId(
@@ -3870,24 +3966,23 @@ export class ClaimRequestManagementService {
}
this.logger.log(
`[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} for nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}`,
`[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} for nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` +
`PolicyNo=${policyNo ?? "NULL"}, PolicyCINumber=${policyCINumber ?? "NULL"}`,
);
return {
BeginDate: insurance.startDate ?? null,
BuiltYear:
Number(inquiryMapped.ModelField ?? inquiryMapped.ModelCii) || Number(inquiryMapped.PrdDte) || null,
ChassisNo:
inquiryMapped.ChassisNumberField ??
inquiryMapped.ShsNum ??
inquiryRaw.ChassisNumberField ??
inquiryRaw.ShsNum ??
null,
BeginDate: beginDate,
BuiltYear: builtYear,
ChassisNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"ShsNum",
"ChassisNumberField",
"shsNam",
]),
DmgHistoryStatus: input.defaults.DmgHistoryStatus,
Desc: this.formatFanavaranSelectedPartsDesc(input.selectedParts),
DmgCaseTypeId: input.defaults.DmgCaseTypeId,
DriverId: driverFanavaranId ?? null,
EndDate: insurance.endDate ?? null,
EndDate: endDate,
EstimateAmount: FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT,
FaultPercent: input.defaults.FaultPercent,
InsuranceCorpId: await this.fanavaranLookupService.resolveInsuranceCorpId(input.clientKey),
@@ -3898,12 +3993,11 @@ export class ClaimRequestManagementService {
LicenceIssuDate: person.driverBirthday ?? "1394/10/13",
LicenceNo: person.driverLicense ?? "1124242",
LicenceTypeId: input.defaults.CulpritLicenceTypeId,
MotorNo:
inquiryMapped.MtrNum ??
inquiryMapped.EngineNumberField ??
inquiryRaw.MtrNum ??
inquiryRaw.EngineNumberField ??
null,
MotorNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"MtrNum",
"EngineNumberField",
"mtrnum",
]),
OwnerId: null,
PlaqueCityId: null,
PlaqueKindId: plate ? input.defaults.PlaqueKindId : null,
@@ -3911,14 +4005,20 @@ export class ClaimRequestManagementService {
PlaqueMiddleCodeId: this.getFanavaranPlateMiddleCode(
plate?.centerAlphabet,
),
PlaqueNo: this.formatFanavaranPlateNo(plate),
PlaqueNo:
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, ["Plk"]) ??
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 ?? "",
PolicyNo: policyNo,
PreviousPolicyEndDate: endDate ?? "",
VehicleKindId: vehicleKindId,
VIN: inquiryMapped.VIN ?? inquiryRaw.VIN ?? null,
VIN: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"VIN",
"vin",
"VinNumberField",
]),
AccidentVehicleUsedId: input.defaults.AccidentVehicleUsedId,
PolicyCINumber: policyCINumber,
};

View File

@@ -369,6 +369,7 @@ export class InquiryRefreshService {
}
next.insurance.policyNumber =
mapped.PrntCmpDocNo ||
mapped.PrntPlcyCmpDocNo ||
mapped.printNumber ||
mapped.insuranceNumber ||
@@ -384,9 +385,17 @@ export class InquiryRefreshService {
? String(financialCeiling)
: next.insurance.financialCeiling;
next.insurance.startDate =
mapped.persianStartDate || mapped.IssueDate || mapped.SatrtDate || next.insurance.startDate;
mapped.StartDate ||
mapped.HBgnDte ||
mapped.persianStartDate ||
mapped.SatrtDate ||
mapped.IssueDate ||
next.insurance.startDate;
next.insurance.endDate =
mapped.persianEndDate || mapped.EndDate || next.insurance.endDate;
mapped.EndDate ||
mapped.HEndDte ||
mapped.persianEndDate ||
next.insurance.endDate;
return next;
}

View File

@@ -1371,11 +1371,23 @@ export class RequestManagementService {
};
if (!party.insurance) party.insurance = {} as any;
party.insurance.policyNumber = inquiryMapped?.PrntPlcyCmpDocNo;
party.insurance.policyNumber =
inquiryMapped?.PrntCmpDocNo ||
inquiryMapped?.PrntPlcyCmpDocNo ||
inquiryMapped?.printNumber ||
inquiryMapped?.insuranceNumber;
party.insurance.company = clientName;
party.insurance.financialCeiling = inquiryMapped?.FinancialCvrCptl;
party.insurance.startDate = inquiryMapped?.IssueDate;
party.insurance.endDate = inquiryMapped?.EndDate;
party.insurance.startDate =
inquiryMapped?.StartDate ||
inquiryMapped?.HBgnDte ||
inquiryMapped?.persianStartDate ||
inquiryMapped?.SatrtDate ||
inquiryMapped?.IssueDate;
party.insurance.endDate =
inquiryMapped?.EndDate ||
inquiryMapped?.HEndDte ||
inquiryMapped?.persianEndDate;
// CAR BODY EXTERNAL API INQUIRY
if (req.type === BlameRequestType.CAR_BODY && role === PartyRole.FIRST) {
@@ -1776,11 +1788,23 @@ export class RequestManagementService {
};
if (!party.insurance) party.insurance = {} as any;
party.insurance.policyNumber = inquiryMapped?.PrntPlcyCmpDocNo;
party.insurance.policyNumber =
inquiryMapped?.PrntCmpDocNo ||
inquiryMapped?.PrntPlcyCmpDocNo ||
inquiryMapped?.printNumber ||
inquiryMapped?.insuranceNumber;
party.insurance.company = clientName;
party.insurance.financialCeiling = inquiryMapped?.FinancialCvrCptl;
party.insurance.startDate = inquiryMapped?.IssueDate;
party.insurance.endDate = inquiryMapped?.EndDate;
party.insurance.startDate =
inquiryMapped?.StartDate ||
inquiryMapped?.HBgnDte ||
inquiryMapped?.persianStartDate ||
inquiryMapped?.SatrtDate ||
inquiryMapped?.IssueDate;
party.insurance.endDate =
inquiryMapped?.EndDate ||
inquiryMapped?.HEndDte ||
inquiryMapped?.persianEndDate;
// Advance workflow
await this.advanceWorkflowToNext(req, stepKey);
@@ -8592,11 +8616,23 @@ export class RequestManagementService {
};
if (!party.insurance) party.insurance = {} as any;
party.insurance.policyNumber = inquiryMapped?.PrntPlcyCmpDocNo;
party.insurance.policyNumber =
inquiryMapped?.PrntCmpDocNo ||
inquiryMapped?.PrntPlcyCmpDocNo ||
inquiryMapped?.printNumber ||
inquiryMapped?.insuranceNumber;
party.insurance.company = clientName;
party.insurance.financialCeiling = inquiryMapped?.FinancialCvrCptl;
party.insurance.startDate = inquiryMapped?.IssueDate;
party.insurance.endDate = inquiryMapped?.EndDate;
party.insurance.startDate =
inquiryMapped?.StartDate ||
inquiryMapped?.HBgnDte ||
inquiryMapped?.persianStartDate ||
inquiryMapped?.SatrtDate ||
inquiryMapped?.IssueDate;
party.insurance.endDate =
inquiryMapped?.EndDate ||
inquiryMapped?.HEndDte ||
inquiryMapped?.persianEndDate;
if (
req.type === BlameRequestType.CAR_BODY &&
@@ -9392,11 +9428,23 @@ export class RequestManagementService {
};
if (!party.insurance) party.insurance = {} as any;
party.insurance.policyNumber = inquiryMapped?.PrntPlcyCmpDocNo;
party.insurance.policyNumber =
inquiryMapped?.PrntCmpDocNo ||
inquiryMapped?.PrntPlcyCmpDocNo ||
inquiryMapped?.printNumber ||
inquiryMapped?.insuranceNumber;
party.insurance.company = clientName;
party.insurance.financialCeiling = inquiryMapped?.FinancialCvrCptl;
party.insurance.startDate = inquiryMapped?.IssueDate;
party.insurance.endDate = inquiryMapped?.EndDate;
party.insurance.startDate =
inquiryMapped?.StartDate ||
inquiryMapped?.HBgnDte ||
inquiryMapped?.persianStartDate ||
inquiryMapped?.SatrtDate ||
inquiryMapped?.IssueDate;
party.insurance.endDate =
inquiryMapped?.EndDate ||
inquiryMapped?.HEndDte ||
inquiryMapped?.persianEndDate;
if (
req.type === BlameRequestType.CAR_BODY &&

View File

@@ -114,4 +114,32 @@ describe("SandHubService inquiry mocks", () => {
expect(mapped.PrntPlcyCmpDocNo).toBe("1405/1143-LEGACY/200/1");
});
it("maps ESG PrntCmpDocNo → PrntPlcyCmpDocNo and preserves begin/end dates", () => {
const mapped = (service as any).mapEsgPolicyByPlateToOldFormat({
success: true,
data: {
PrntCmpDocNo: "1110/111130/404/003412",
LastCmpDocNo: "1110/111130/403/002650",
PlcyUnqCod: "16018466143",
HBgnDte: "1404/10/23",
HEndDte: "1405/10/23",
HIsuDte: "1404/10/21",
VIN: "IRFC891V7D2301065",
MtrNum: "13389030167",
ShsNum: "NAAP41FD5BJ301065",
PrdDte: "1389",
CmpCod: "8",
CmpNam: "بيمه پارسيان",
},
});
expect(mapped.PrntPlcyCmpDocNo).toBe("1110/111130/404/003412");
expect(mapped.ThirdPolicyCode).toBe("16018466143");
expect(mapped.StartDate).toBe("1404/10/23");
expect(mapped.EndDate).toBe("1405/10/23");
expect(mapped.IssueDate).toBe("1404/10/21");
expect(mapped.VIN).toBe("IRFC891V7D2301065");
expect(mapped.ModelField).toBe("1389");
});
});

View File

@@ -560,14 +560,22 @@ export class SandHubService {
mtrnum: data.MtrNum ?? data.mtrnum,
shsNam: data.ShsNum ?? data.ShsNam ?? data.shsNam,
vin: data.VIN ?? data.vin,
VIN: data.VIN ?? data.vin,
ntnlId: data.NtnlId ?? data.ntnlId,
fullname: data.InsNam ?? data.fullname,
ThirdPolicyCode: data.PlcyUnqCod ?? data.ThirdPolicyCode,
// ESG print/policy document number — canonical Tejarat name is PrntPlcyCmpDocNo
PrntPlcyCmpDocNo:
data.PrntCmpDocNo ??
data.PrntPlcyCmpDocNo ??
data.printNumber ??
data.insuranceNumber,
LastCompanyDocumentNumber:
data.LastCmpDocNo ?? data.LastCompanyDocumentNumber,
IssueDate: data.HIsuDte ?? data.IssueDate,
StartDate: data.HBgnDte ?? data.StartDate,
EndDate: data.HEndDte ?? data.EndDate,
ModelField: data.PrdDte ?? data.ModelField ?? data.ModelCii,
EdrsJson: data.Edrses ?? data.EdrsJson,
});
}
@@ -1010,14 +1018,26 @@ export class SandHubService {
FinancialCvrCptl:
newResponse.financeCoverage || newResponse.FinancialCvrCptl || "0",
// Dates
IssueDate: newResponse.persianStartDate || newResponse.IssueDate,
EndDate: newResponse.persianEndDate || newResponse.EndDate,
// Dates — keep IssueDate (HIsuDte) distinct from StartDate (HBgnDte)
IssueDate:
newResponse.IssueDate ||
newResponse.HIsuDte ||
newResponse.persianStartDate,
StartDate:
newResponse.StartDate ||
newResponse.HBgnDte ||
newResponse.SatrtDate ||
newResponse.persianStartDate,
EndDate:
newResponse.EndDate ||
newResponse.HEndDte ||
newResponse.persianEndDate,
// Insurance policy number — canonical Tejarat name is PrntPlcyCmpDocNo;
// newer API formats surface it as printNumber or insuranceNumber.
// ESG uses PrntCmpDocNo; newer formats use printNumber / insuranceNumber.
PrntPlcyCmpDocNo:
newResponse.PrntPlcyCmpDocNo ||
newResponse.PrntCmpDocNo ||
newResponse.printNumber ||
newResponse.insuranceNumber ||
null,