forked from Yara724/api
fix: harden claim review and inquiry workflows
Preserve damage history and current vehicle price, restore depreciation mapping, normalize inquiry/report output, and support resumable expert review with paginated case retrieval.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { SandHubService } from "./sand-hub.service";
|
||||
import {
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
ServiceUnavailableException,
|
||||
} from "@nestjs/common";
|
||||
import { ExternalInquirySettingsService } from "src/client/external-inquiry-settings.service";
|
||||
@@ -58,7 +59,9 @@ describe("SandHubService inquiry mocks", () => {
|
||||
});
|
||||
|
||||
it("keeps disabled-live third-party mock policy usable", async () => {
|
||||
const result = await service.getTejaratBlockInquiry(userDetail);
|
||||
const result = await service.getTejaratBlockInquiry(userDetail, {
|
||||
enforceDeploymentClientMatch: true,
|
||||
});
|
||||
|
||||
expect(isMappedPolicyCurrent(result.mapped)).toBe(true);
|
||||
});
|
||||
@@ -121,6 +124,17 @@ describe("SandHubService inquiry mocks", () => {
|
||||
expect(httpService.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a contextual Persian error when no car-body policy matches", async () => {
|
||||
externalInquirySettings.isInquiryLive.mockResolvedValue(true);
|
||||
lookupsService.findLastProcessedCarPolicy.mockRejectedValue(
|
||||
new NotFoundException("No active Fanavaran car-body policy was found"),
|
||||
);
|
||||
|
||||
await expect(service.getCarBodyInquiry(userDetail)).rejects.toThrow(
|
||||
"بیمهنامه بدنه فعالی مطابق پلاک و کد ملی واردشده یافت نشد.",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not let an offline third-party seed override a live ESG inquiry", async () => {
|
||||
process.env.CLIENT_ID = "8";
|
||||
externalInquirySettings.isInquiryLive.mockResolvedValue(true);
|
||||
@@ -129,12 +143,10 @@ describe("SandHubService inquiry mocks", () => {
|
||||
raw: { mocked: true },
|
||||
mapped: { PrntPlcyCmpDocNo: "MOCK-POLICY" },
|
||||
});
|
||||
const esg = jest
|
||||
.spyOn(service as any, "makeEsgRequest")
|
||||
.mockResolvedValue({
|
||||
success: true,
|
||||
data: { PrntCmpDocNo: "REAL-ESG-POLICY" },
|
||||
});
|
||||
const esg = jest.spyOn(service as any, "makeEsgRequest").mockResolvedValue({
|
||||
success: true,
|
||||
data: { PrntCmpDocNo: "REAL-ESG-POLICY" },
|
||||
});
|
||||
|
||||
const result = await service.getTejaratBlockInquiry(userDetail);
|
||||
|
||||
@@ -143,6 +155,40 @@ describe("SandHubService inquiry mocks", () => {
|
||||
expect(result.mapped.PrntPlcyCmpDocNo).toBe("REAL-ESG-POLICY");
|
||||
});
|
||||
|
||||
it("preserves ESG not-found semantics as a Persian plate-specific error", async () => {
|
||||
process.env.CLIENT_ID = "8";
|
||||
externalInquirySettings.isInquiryLive.mockResolvedValue(true);
|
||||
jest.spyOn(service as any, "makeEsgRequest").mockResolvedValue({
|
||||
success: false,
|
||||
message: "موردی یافت نشد",
|
||||
});
|
||||
|
||||
const result = await service.getTejaratBlockInquiry(userDetail, {
|
||||
enforceDeploymentClientMatch: true,
|
||||
});
|
||||
|
||||
expect(result.mapped.Error.Message).toBe(
|
||||
"بیمهنامه شخص ثالثی مطابق پلاک و کد ملی واردشده یافت نشد.",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a VIN-specific message for the same ESG not-found response", async () => {
|
||||
externalInquirySettings.isInquiryLive.mockResolvedValue(true);
|
||||
jest.spyOn(service as any, "makeEsgRequest").mockResolvedValue({
|
||||
success: false,
|
||||
message: "موردی یافت نشد",
|
||||
});
|
||||
|
||||
const result = await service.getPolicyByChassisInquiry({
|
||||
nationalCode: "0012345678",
|
||||
chassis: "NAAR03HFFRDE07024",
|
||||
});
|
||||
|
||||
expect(result.mapped.Error.Message).toBe(
|
||||
"بیمهنامه شخص ثالثی مطابق شماره شاسی (VIN) و کد ملی واردشده یافت نشد.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a guilty third-party policy issued by another insurer", async () => {
|
||||
process.env.CLIENT_ID = "15";
|
||||
externalInquirySettings.isInquiryLive.mockResolvedValue(true);
|
||||
|
||||
@@ -24,6 +24,13 @@ import { jalaliToGregorianDate } from "src/helpers/date-jalali";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { mapEsgCarBodyPolicyToInquiry } from "./esg-car-body-inquiry.mapper";
|
||||
import type { Plates } from "src/Types&Enums/plate.interface";
|
||||
import {
|
||||
getInquiryErrorMessage,
|
||||
inquiryErrorStatus,
|
||||
isInquiryFailurePayload,
|
||||
isInquiryTimeout,
|
||||
type InquiryErrorContext,
|
||||
} from "src/common/utils/inquiry-error";
|
||||
|
||||
type CarBodyInquiryDetail = Omit<SandHubDetailDto, "plate"> & {
|
||||
plate: Plates | string;
|
||||
@@ -31,9 +38,6 @@ type CarBodyInquiryDetail = Omit<SandHubDetailDto, "plate"> & {
|
||||
|
||||
@Injectable()
|
||||
export class SandHubService {
|
||||
private static readonly ESG_INQUIRY_UNAVAILABLE_MESSAGE =
|
||||
"استعلام در دسترس نیست";
|
||||
|
||||
private readonly logger = new Logger(SandHubService.name);
|
||||
private loginToken: string | null = null;
|
||||
private tokenExpiry: Date | null = null;
|
||||
@@ -90,6 +94,30 @@ export class SandHubService {
|
||||
return resolveFanavaranClientKey() === "parsian";
|
||||
}
|
||||
|
||||
private inquiryContext(type: ExternalInquiryType): InquiryErrorContext {
|
||||
if (type === "vinChassis") return "thirdPartyVin";
|
||||
return type;
|
||||
}
|
||||
|
||||
private throwInquiryError(
|
||||
error: unknown,
|
||||
context: InquiryErrorContext,
|
||||
): never {
|
||||
const message = getInquiryErrorMessage(error, context);
|
||||
const status = inquiryErrorStatus(error);
|
||||
|
||||
if (error instanceof ForbiddenException || status === 403) {
|
||||
throw new ForbiddenException(message);
|
||||
}
|
||||
if (isInquiryTimeout(error) || status === 504) {
|
||||
throw new GatewayTimeoutException(message);
|
||||
}
|
||||
if ([400, 404, 409, 422].includes(status ?? 0)) {
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
throw new ServiceUnavailableException(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* A case may proceed only when the policy belongs to the insurer served by
|
||||
* this deployment. Callers opt in for the guilty/first-party policy only.
|
||||
@@ -101,7 +129,7 @@ export class SandHubService {
|
||||
const expectedClientCode = String(process.env.CLIENT_ID ?? "").trim();
|
||||
if (!expectedClientCode) {
|
||||
throw new ServiceUnavailableException(
|
||||
"CLIENT_ID must be configured before insurance eligibility can be checked.",
|
||||
"تنظیمات شرکت بیمه برای بررسی اعتبار بیمهنامه کامل نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +139,7 @@ export class SandHubService {
|
||||
if (actualClientCode === expectedClientCode) return;
|
||||
|
||||
throw new ForbiddenException(
|
||||
`${insuranceLine} policy insurer does not match this deployment.`,
|
||||
"بیمهنامه یافتشده متعلق به شرکت بیمه این سامانه نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,13 +162,13 @@ export class SandHubService {
|
||||
const expectedClientCode = String(process.env.CLIENT_ID ?? "").trim();
|
||||
if (!expectedClientCode) {
|
||||
throw new ServiceUnavailableException(
|
||||
"CLIENT_ID must be configured before insurance eligibility can be checked.",
|
||||
"تنظیمات شرکت بیمه برای بررسی اعتبار بیمهنامه کامل نیست.",
|
||||
);
|
||||
}
|
||||
if (expectedClientCode === "8") return;
|
||||
|
||||
throw new ForbiddenException(
|
||||
"CAR_BODY policy insurer does not match this deployment.",
|
||||
"بیمهنامه یافتشده متعلق به شرکت بیمه این سامانه نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -426,7 +454,7 @@ export class SandHubService {
|
||||
this.logger.error("Failed to login to SandHub:", er.message);
|
||||
this.loginToken = null;
|
||||
this.tokenExpiry = null;
|
||||
throw new UnauthorizedException("SandHub authentication failed");
|
||||
throw new UnauthorizedException("احراز هویت سرویس استعلام انجام نشد.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,7 +474,7 @@ export class SandHubService {
|
||||
|
||||
if (!email || !password) {
|
||||
throw new UnauthorizedException(
|
||||
"Tejarat inquiry credentials are not configured (TEJARAT_INQUIRY_EMAIL/TEJARAT_INQUIRY_PASSWORD)",
|
||||
"اطلاعات اتصال به سرویس استعلام تجارت نو تنظیم نشده است.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -486,12 +514,18 @@ export class SandHubService {
|
||||
);
|
||||
this.tejaratAccessToken = null;
|
||||
this.tejaratTokenExpiry = null;
|
||||
throw new UnauthorizedException("Tejarat inquiry authentication failed");
|
||||
throw new UnauthorizedException(
|
||||
"احراز هویت سرویس استعلام تجارت نو انجام نشد.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getEsgAccessToken(): Promise<string> {
|
||||
if (this.esgAccessToken && this.esgTokenExpiry && this.esgTokenExpiry > new Date()) {
|
||||
if (
|
||||
this.esgAccessToken &&
|
||||
this.esgTokenExpiry &&
|
||||
this.esgTokenExpiry > new Date()
|
||||
) {
|
||||
return this.esgAccessToken;
|
||||
}
|
||||
|
||||
@@ -501,7 +535,7 @@ export class SandHubService {
|
||||
|
||||
if (!baseUrl || !username || !password) {
|
||||
throw new UnauthorizedException(
|
||||
"ESG credentials are not configured (ESG_URL/ESG_USERNAME/ESG_PASSWORD)",
|
||||
"اطلاعات اتصال به سرویس استعلام ESG تنظیم نشده است.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -539,7 +573,9 @@ export class SandHubService {
|
||||
this.logger.error("Failed to login to ESG inquiry:", er?.message || er);
|
||||
this.esgAccessToken = null;
|
||||
this.esgTokenExpiry = null;
|
||||
throw new UnauthorizedException("ESG inquiry authentication failed");
|
||||
throw new UnauthorizedException(
|
||||
"احراز هویت سرویس استعلام ESG انجام نشد.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,24 +639,30 @@ export class SandHubService {
|
||||
this.esgTokenExpiry = null;
|
||||
}
|
||||
|
||||
if ([400, 404, 409, 422].includes(status)) {
|
||||
this.throwInquiryError(err, this.inquiryContext(inquiryType));
|
||||
}
|
||||
|
||||
if (attempt === maxRetries - 1) {
|
||||
this.throwInquiryError(err, this.inquiryContext(inquiryType));
|
||||
}
|
||||
const delay = INITIAL_DELAY * Math.pow(BACKOFF_FACTOR, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
if (attempt === maxRetries - 1) throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private mapEsgPolicyByPlateToOldFormat(raw: any): any {
|
||||
private mapEsgPolicyByPlateToOldFormat(
|
||||
raw: any,
|
||||
context: "thirdPartyPlate" | "thirdPartyVin" = "thirdPartyPlate",
|
||||
): any {
|
||||
if (!raw) return raw;
|
||||
|
||||
if (raw?.success === false) {
|
||||
this.logger.warn(
|
||||
"ESG policyByPlate inquiry returned success=false",
|
||||
raw,
|
||||
);
|
||||
if (isInquiryFailurePayload(raw)) {
|
||||
this.logger.warn("ESG policy inquiry returned a failure payload", raw);
|
||||
return {
|
||||
Error: {
|
||||
Message: SandHubService.ESG_INQUIRY_UNAVAILABLE_MESSAGE,
|
||||
Message: getInquiryErrorMessage(raw, context),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -670,7 +712,8 @@ export class SandHubService {
|
||||
): string | null {
|
||||
if (input === null || input === undefined) return null;
|
||||
|
||||
const raw = typeof input === "number" ? String(input) : String(input).trim();
|
||||
const raw =
|
||||
typeof input === "number" ? String(input) : String(input).trim();
|
||||
if (!raw) return null;
|
||||
|
||||
let year = 0;
|
||||
@@ -700,7 +743,9 @@ export class SandHubService {
|
||||
return `${year}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
private getDefaultMockPersonInquiry(nationalCode: string): Record<string, unknown> {
|
||||
private getDefaultMockPersonInquiry(
|
||||
nationalCode: string,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
firstName: "نام",
|
||||
lastName: "خانوادگی",
|
||||
@@ -711,10 +756,10 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
private mapEsgPersonInquiryToOldFormat(raw: any): Record<string, unknown> {
|
||||
if (raw?.success === false) {
|
||||
if (isInquiryFailurePayload(raw)) {
|
||||
this.logger.warn("ESG person inquiry returned success=false", raw);
|
||||
throw new BadRequestException(
|
||||
SandHubService.ESG_INQUIRY_UNAVAILABLE_MESSAGE,
|
||||
getInquiryErrorMessage(raw, "personalIdentity"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -736,11 +781,9 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
private mapEsgShebaInquiryToOldFormat(raw: any): Record<string, unknown> {
|
||||
if (raw?.success === false) {
|
||||
if (isInquiryFailurePayload(raw)) {
|
||||
this.logger.warn("ESG sheba inquiry returned success=false", raw);
|
||||
throw new BadRequestException(
|
||||
SandHubService.ESG_INQUIRY_UNAVAILABLE_MESSAGE,
|
||||
);
|
||||
throw new BadRequestException(getInquiryErrorMessage(raw, "sheba"));
|
||||
}
|
||||
|
||||
const data = raw?.data ?? {};
|
||||
@@ -800,11 +843,15 @@ export class SandHubService {
|
||||
this.tejaratTokenExpiry = null;
|
||||
}
|
||||
|
||||
if ([400, 404, 409, 422].includes(status)) {
|
||||
this.throwInquiryError(err, this.inquiryContext(inquiryType));
|
||||
}
|
||||
|
||||
if (attempt === maxRetries - 1) {
|
||||
this.throwInquiryError(err, this.inquiryContext(inquiryType));
|
||||
}
|
||||
const delay = INITIAL_DELAY * Math.pow(BACKOFF_FACTOR, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
if (attempt === maxRetries - 1) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -883,8 +930,13 @@ export class SandHubService {
|
||||
`[MOCK] getEsgPolicyByPlateInquiry plate=${JSON.stringify(requestPayload)}`,
|
||||
);
|
||||
}
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw);
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(
|
||||
raw,
|
||||
"thirdPartyPlate",
|
||||
);
|
||||
if (!mapped?.Error) {
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
}
|
||||
return { raw, mapped };
|
||||
}
|
||||
|
||||
@@ -912,8 +964,16 @@ export class SandHubService {
|
||||
`[MOCK] getTejaratBlockInquiry plate=${JSON.stringify(requestPayload)}`,
|
||||
);
|
||||
}
|
||||
const mapped = this.mapNewApiResponseToOldFormat(raw);
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
const mapped = isInquiryFailurePayload(raw)
|
||||
? {
|
||||
Error: {
|
||||
Message: getInquiryErrorMessage(raw, "thirdPartyPlate"),
|
||||
},
|
||||
}
|
||||
: this.mapNewApiResponseToOldFormat(raw);
|
||||
if (!mapped?.Error) {
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
}
|
||||
return { raw, mapped };
|
||||
}
|
||||
|
||||
@@ -962,13 +1022,15 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
if (isVinInquiry) {
|
||||
const raw = await this.lookupsService.findLastProcessedCarPolicy(
|
||||
"car-body",
|
||||
{
|
||||
let raw: any;
|
||||
try {
|
||||
raw = await this.lookupsService.findLastProcessedCarPolicy("car-body", {
|
||||
nationalCode: String(userDetail.nationalCodeOfInsurer),
|
||||
vin: plateOrVin,
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
this.throwInquiryError(error, "carBodyVin");
|
||||
}
|
||||
if (useParsianCarBodyLookup) {
|
||||
this.assertParsianCarBodyLookupMatchesDeployment();
|
||||
}
|
||||
@@ -1000,10 +1062,15 @@ export class SandHubService {
|
||||
plaqueRight: String(plateOrVin.centerDigits),
|
||||
plaqueSerial: String(plateOrVin.ir),
|
||||
};
|
||||
const raw = await this.lookupsService.findLastProcessedCarPolicy(
|
||||
"car-body",
|
||||
query,
|
||||
);
|
||||
let raw: any;
|
||||
try {
|
||||
raw = await this.lookupsService.findLastProcessedCarPolicy(
|
||||
"car-body",
|
||||
query,
|
||||
);
|
||||
} catch (error) {
|
||||
this.throwInquiryError(error, "carBodyPlate");
|
||||
}
|
||||
this.assertParsianCarBodyLookupMatchesDeployment();
|
||||
|
||||
return {
|
||||
@@ -1046,6 +1113,11 @@ export class SandHubService {
|
||||
options,
|
||||
);
|
||||
|
||||
if (isInquiryFailurePayload(raw)) {
|
||||
throw new BadRequestException(
|
||||
getInquiryErrorMessage(raw, "carBodyPlate"),
|
||||
);
|
||||
}
|
||||
const mapped = this.mapCarBodyInquiryResponse(raw);
|
||||
return { raw, mapped };
|
||||
}
|
||||
@@ -1114,7 +1186,6 @@ export class SandHubService {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`).
|
||||
*
|
||||
@@ -1130,10 +1201,10 @@ export class SandHubService {
|
||||
options?: SandHubInquiryOptions,
|
||||
): Promise<{ raw: any; mapped: any }> {
|
||||
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
||||
const requestUrl = `${baseUrl}/inquiry/carByChassis`;
|
||||
const requestUrl = `${baseUrl}/inquiry/policyByChassis`;
|
||||
const requestPayload = {
|
||||
nationalCode: String(identity.nationalCode),
|
||||
chassisNo: String(identity.chassis),
|
||||
chassis: String(identity.chassis),
|
||||
};
|
||||
|
||||
const live = await this.isInquiryLive("vinChassis", options);
|
||||
@@ -1144,8 +1215,10 @@ export class SandHubService {
|
||||
this.logger.debug(
|
||||
`[MOCK] getPolicyByChassisInquiry nationalCode=${identity.nationalCode} chassis=${identity.chassis}`,
|
||||
);
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw);
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw, "thirdPartyVin");
|
||||
if (!mapped?.Error) {
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
}
|
||||
return { raw, mapped };
|
||||
}
|
||||
|
||||
@@ -1155,12 +1228,13 @@ export class SandHubService {
|
||||
"vinChassis",
|
||||
options,
|
||||
);
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw);
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw, "thirdPartyVin");
|
||||
if (!mapped?.Error) {
|
||||
this.enforceDeploymentClientMatch(mapped, "THIRD_PARTY", options);
|
||||
}
|
||||
return { raw, mapped };
|
||||
}
|
||||
|
||||
|
||||
private async makeSandHubRequest(
|
||||
url: string,
|
||||
payload: any,
|
||||
@@ -1205,7 +1279,7 @@ export class SandHubService {
|
||||
}
|
||||
}
|
||||
throw new BadGatewayException(
|
||||
"Failed to fetch data from SandHub after multiple retries",
|
||||
"سرویس استعلام پس از چند تلاش پاسخ نداد. لطفاً کمی بعد دوباره تلاش کنید.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1229,11 +1303,16 @@ export class SandHubService {
|
||||
// Pattern: {centerDigits}{centerLetter(s)}{leftDigits}<space>{ir}
|
||||
const m = plk.trim().match(/^(\d+)([^\d\s]+)(\d+)\s+(\d+)$/);
|
||||
if (!m) return null;
|
||||
const Plk3 = parseInt(m[1], 10); // center digits
|
||||
const Plk3 = parseInt(m[1], 10); // center digits
|
||||
const Plk2 = this.plateNormalizer.normalizePlateText(m[2]);
|
||||
const Plk1 = parseInt(m[3], 10); // left digits
|
||||
const Plk1 = parseInt(m[3], 10); // left digits
|
||||
const PlkSrl = parseInt(m[4], 10); // IR region code
|
||||
if (!Number.isFinite(Plk3) || !Number.isFinite(Plk1) || !Number.isFinite(PlkSrl) || !Plk2) {
|
||||
if (
|
||||
!Number.isFinite(Plk3) ||
|
||||
!Number.isFinite(Plk1) ||
|
||||
!Number.isFinite(PlkSrl) ||
|
||||
!Plk2
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { Plk1, Plk2, Plk3, PlkSrl };
|
||||
@@ -1245,7 +1324,12 @@ export class SandHubService {
|
||||
// If the response carries a `plk` plate string (VIN inquiry) but lacks the
|
||||
// individual Plk1/Plk2/Plk3/PlkSrl fields, parse and inject them so that
|
||||
// all downstream plate-handling code works identically to the plate flow.
|
||||
let plkParts: { Plk1: number; Plk2: string; Plk3: number; PlkSrl: number } | null = null;
|
||||
let plkParts: {
|
||||
Plk1: number;
|
||||
Plk2: string;
|
||||
Plk3: number;
|
||||
PlkSrl: number;
|
||||
} | null = null;
|
||||
if (
|
||||
newResponse.plk &&
|
||||
newResponse.Plk1 == null &&
|
||||
@@ -1258,13 +1342,15 @@ export class SandHubService {
|
||||
// Map the new field names to the old field names
|
||||
return {
|
||||
...newResponse,
|
||||
...(plkParts ? {
|
||||
Plk1: plkParts.Plk1,
|
||||
Plk2: plkParts.Plk2,
|
||||
Plk3: plkParts.Plk3,
|
||||
PlkSrl: plkParts.PlkSrl,
|
||||
plateLetterid: plkParts.Plk2,
|
||||
} : {}),
|
||||
...(plkParts
|
||||
? {
|
||||
Plk1: plkParts.Plk1,
|
||||
Plk2: plkParts.Plk2,
|
||||
Plk3: plkParts.Plk3,
|
||||
PlkSrl: plkParts.PlkSrl,
|
||||
plateLetterid: plkParts.Plk2,
|
||||
}
|
||||
: {}),
|
||||
// Company information
|
||||
CompanyCode: newResponse.companyId || newResponse.CompanyCode,
|
||||
CompanyName: newResponse.companyPersianName || newResponse.CompanyName,
|
||||
@@ -1401,7 +1487,7 @@ export class SandHubService {
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
throw new Error(err);
|
||||
this.throwInquiryError(err, "thirdPartyPlate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1420,7 +1506,7 @@ export class SandHubService {
|
||||
const jalaliBirthDate = this.normalizeJalaliBirthDateForEsg(birthDate);
|
||||
if (!jalaliBirthDate) {
|
||||
throw new BadRequestException(
|
||||
`Invalid birth date for personal inquiry: ${birthDate}. Expected a Jalali date (e.g. 13781124 or "1378-11-24").`,
|
||||
"تاریخ تولد واردشده برای استعلام هویت معتبر نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1454,7 +1540,7 @@ export class SandHubService {
|
||||
const gregorianBirthdate = jalaliToGregorianDate(birthDate);
|
||||
if (!gregorianBirthdate) {
|
||||
throw new BadRequestException(
|
||||
`Invalid birth date for personal inquiry: ${birthDate}. Expected a Jalali date (e.g. 13770624 or "1377-06-24").`,
|
||||
"تاریخ تولد واردشده برای استعلام هویت معتبر نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1472,7 +1558,7 @@ export class SandHubService {
|
||||
|
||||
if (response?.message?.includes("err.record.not.found")) {
|
||||
throw new NotFoundException(
|
||||
"Personal inquiry failed: Record not found for the given national code and birth date.",
|
||||
getInquiryErrorMessage(response, "personalIdentity"),
|
||||
);
|
||||
}
|
||||
return response.data;
|
||||
@@ -1483,7 +1569,7 @@ export class SandHubService {
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
throw new Error(`Error in finding personal inquiry: ${err}`);
|
||||
this.throwInquiryError(err, "personalIdentity");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1512,22 +1598,13 @@ export class SandHubService {
|
||||
|
||||
if (response?.data?.IsSucceed === false) {
|
||||
throw new NotFoundException(
|
||||
"Driving license check failed: The license is not valid or could not be found.",
|
||||
"گواهینامهای مطابق کد ملی و شماره گواهینامه واردشده یافت نشد.",
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BadGatewayException &&
|
||||
error.message.includes("multiple retries")
|
||||
) {
|
||||
throw new BadGatewayException(
|
||||
`Driving license check failed after multiple retries. The service may be down.`,
|
||||
);
|
||||
}
|
||||
// For all other errors (like 400, 404, etc.), re-throw them as-is.
|
||||
throw new Error(`Error in finding driving license: ${error}`);
|
||||
this.throwInquiryError(error, "drivingLicense");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1563,13 +1640,13 @@ export class SandHubService {
|
||||
response,
|
||||
);
|
||||
throw new BadRequestException(
|
||||
"Ownership validation failed: The provided national ID is not the owner of this vehicle.",
|
||||
"پلاک واردشده متعلق به کد ملی واردشده نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
throw new Error(`Error in finding car ownership: ${err}`);
|
||||
this.throwInquiryError(err, "carOwnership");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1619,7 +1696,7 @@ export class SandHubService {
|
||||
response,
|
||||
);
|
||||
throw new BadRequestException(
|
||||
"Sheba ID validation failed. The provided Sheba ID does not match the national ID.",
|
||||
"شماره شبا متعلق به کد ملی واردشده نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1647,7 +1724,7 @@ export class SandHubService {
|
||||
response,
|
||||
);
|
||||
throw new BadRequestException(
|
||||
"Sheba ID validation failed. The provided Sheba ID does not match the national ID.",
|
||||
"شماره شبا متعلق به کد ملی واردشده نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1656,7 +1733,7 @@ export class SandHubService {
|
||||
if (err instanceof BadRequestException) {
|
||||
throw err;
|
||||
}
|
||||
throw new Error(`Error in matching sheba validation: ${err}`);
|
||||
this.throwInquiryError(err, "sheba");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1672,9 +1749,7 @@ export class SandHubService {
|
||||
);
|
||||
|
||||
if (err.response.status === 400) {
|
||||
throw new BadGatewayException(
|
||||
`SandHub rejected the request with a 400 Bad Request. Details: ${JSON.stringify(err.response.data)}`,
|
||||
);
|
||||
throw new BadGatewayException(getInquiryErrorMessage(err, "generic"));
|
||||
}
|
||||
} else {
|
||||
this.logger.error(
|
||||
@@ -1685,23 +1760,24 @@ export class SandHubService {
|
||||
|
||||
if (err.message === "EMPTY_RESPONSE") {
|
||||
throw new BadGatewayException(
|
||||
"SandHub is offline or returned an empty response",
|
||||
"سرویس استعلام پاسخی برنگرداند. لطفاً دوباره تلاش کنید.",
|
||||
);
|
||||
}
|
||||
if (err.code === "ECONNABORTED") {
|
||||
throw new GatewayTimeoutException("SandHub request timed out");
|
||||
throw new GatewayTimeoutException(
|
||||
"زمان پاسخگویی سرویس استعلام به پایان رسید. لطفاً دوباره تلاش کنید.",
|
||||
);
|
||||
}
|
||||
if (err.code === "ECONNRESET" || err.message.includes("socket hang up")) {
|
||||
throw new ServiceUnavailableException(
|
||||
"SandHub connection was reset or closed unexpectedly",
|
||||
"ارتباط با سرویس استعلام قطع شد. لطفاً دوباره تلاش کنید.",
|
||||
);
|
||||
}
|
||||
|
||||
// This final check is for when all retries have failed for a retryable error.
|
||||
if (attempt >= maxRetries) {
|
||||
if (attempt >= maxRetries - 1) {
|
||||
throw new BadGatewayException(
|
||||
"Failed to fetch data from SandHub after multiple retries",
|
||||
err.message,
|
||||
"سرویس استعلام پس از چند تلاش پاسخ نداد. لطفاً کمی بعد دوباره تلاش کنید.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user