forked from Yara724/api
1344 lines
42 KiB
TypeScript
1344 lines
42 KiB
TypeScript
import { HttpService } from "@nestjs/axios";
|
|
import {
|
|
BadGatewayException,
|
|
BadRequestException,
|
|
GatewayTimeoutException,
|
|
Injectable,
|
|
Logger,
|
|
NotFoundException,
|
|
ServiceUnavailableException,
|
|
UnauthorizedException,
|
|
} from "@nestjs/common";
|
|
import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service";
|
|
import { SandHubModel } from "src/sand-hub/entity/schema/sand-hub.schema";
|
|
import { ExternalInquirySettingsService } from "src/client/external-inquiry-settings.service";
|
|
import type { ExternalInquiryType } from "src/common/types/external-inquiry.types";
|
|
import type { MockInquiryCompanyContext } from "src/common/types/external-inquiry.types";
|
|
import { SandHubDetailDto, SandHubInquiryOptions } from "./dto/sand-hub.dto";
|
|
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
|
|
import { firstValueFrom } from "rxjs";
|
|
|
|
@Injectable()
|
|
export class SandHubService {
|
|
private readonly logger = new Logger(SandHubService.name);
|
|
private loginToken: string | null = null;
|
|
private tokenExpiry: Date | null = null;
|
|
|
|
// Tejarat inquiry auth (82.99.202.245:3027)
|
|
private tejaratAccessToken: string | null = null;
|
|
private tejaratTokenExpiry: Date | null = null;
|
|
|
|
// ESG inquiry auth (used for selected tenants, e.g. CLIENT_ID=8)
|
|
private esgAccessToken: string | null = null;
|
|
private esgTokenExpiry: Date | null = null;
|
|
|
|
constructor(
|
|
private readonly httpService: HttpService,
|
|
private readonly sandHubDbService: SandHubDbService,
|
|
private readonly externalInquirySettings: ExternalInquirySettingsService,
|
|
) {}
|
|
|
|
private clientRefFrom(options?: SandHubInquiryOptions): string | undefined {
|
|
return options?.clientId;
|
|
}
|
|
|
|
private async isInquiryLive(
|
|
type: ExternalInquiryType,
|
|
options?: SandHubInquiryOptions,
|
|
): Promise<boolean> {
|
|
return this.externalInquirySettings.isInquiryLive(
|
|
type,
|
|
this.clientRefFrom(options),
|
|
);
|
|
}
|
|
|
|
private async mockCompanyContext(
|
|
options?: SandHubInquiryOptions,
|
|
): Promise<MockInquiryCompanyContext> {
|
|
return this.externalInquirySettings.getMockCompanyContext(
|
|
this.clientRefFrom(options),
|
|
);
|
|
}
|
|
|
|
private shouldUseEsgInquiryProvider(): boolean {
|
|
return String(process.env.CLIENT_ID ?? "") === "8";
|
|
}
|
|
|
|
/** Fixed plate/insurance inquiry payload used everywhere we mock block-inquiry style APIs. */
|
|
private buildMockPlateInquiryRaw(
|
|
ctx: MockInquiryCompanyContext,
|
|
): Record<string, unknown> {
|
|
return {
|
|
PrntPlcyCmpDocNo: "1404/1143-70591/200/123",
|
|
MapTypNam: "فائو ( FAW )",
|
|
MtrNum: "TZ196XYAP223A210074",
|
|
ShsNum: "LFP8C7PC3R1K12157",
|
|
DisFnYrNum: null,
|
|
DisLfYrNum: null,
|
|
DisPrsnYrNum: null,
|
|
DisPrsnYrPrcnt: null,
|
|
DisFnYrPrcnt: "5",
|
|
DisLfYrPrcnt: "5",
|
|
vin: "LFP8C7PC3R1K12157",
|
|
MapVehicleSystemName: "ثبت نشده",
|
|
LfCvrCptl: 0,
|
|
FnCvrCptl: 0,
|
|
PrsnCvrCptl: 0,
|
|
VehicleSystemCode: 1,
|
|
EdrsJson: "",
|
|
PersonCvrCptl: 12000000000,
|
|
LifeCvrCptl: 16000000000,
|
|
FinancialCvrCptl: 4000000000,
|
|
CarGroupCode: 3,
|
|
CylCnt: 4,
|
|
LastCompanyDocumentNumber: "31/3100/03/18350",
|
|
UsageCode: "8",
|
|
MapUsageCode: 1,
|
|
MapUsageName: "شخصي",
|
|
Plk1: 16,
|
|
Plk2: 12,
|
|
Plk3: 498,
|
|
PlkSrl: 60,
|
|
PrintEndorsCompanyDocumentNumber: "",
|
|
EndorseDate: null,
|
|
InsuranceFullName: null,
|
|
SystemField: "سايپا",
|
|
TypeField: "111SE",
|
|
UsageField: "سواري",
|
|
MainColorField: "سفيد",
|
|
SecondColorField: "سفيد شيري",
|
|
ModelField: "2024",
|
|
CapacityField: "جمعا 4 نفر",
|
|
CylinderNumberField: "4",
|
|
EngineNumberField: "5215907",
|
|
ChassisNumberField: "LFP8C7PC3R1K12157",
|
|
VinNumberField: "LFP8C7PC3R1K12157",
|
|
InstallDateField: "1403/03/01",
|
|
AxelNumberField: "2",
|
|
WheelNumberField: "4",
|
|
CompanyName: ctx.companyName,
|
|
CompanyCode: ctx.companyId,
|
|
IssueDate: "1403/04/06",
|
|
SatrtDate: "1403/04/06",
|
|
EndDate: "1404/04/06",
|
|
Thrname: "",
|
|
EndorseText: null,
|
|
PolicyHealthLossCount: 0,
|
|
PolicyFinancialLossCount: 0,
|
|
PolicyPersonLossCount: 0,
|
|
Tonage: 0,
|
|
ThirdPolicyCode: 10502330579,
|
|
DiscountPersonPercent: null,
|
|
DiscountThirdPercent: null,
|
|
SystemCodeCii: 0,
|
|
SystemNameCii: "ثبت نشده",
|
|
TypeCodeCii: 12189,
|
|
TypeNameCii: "فائو ( FAW )",
|
|
UsageNameCii: "سواري",
|
|
UsageCodeCii: 1,
|
|
ModelCii: 2024,
|
|
damageTypes: [],
|
|
StatusTypeCode: 1,
|
|
};
|
|
}
|
|
|
|
private buildMockCarBodyInquiryRaw(
|
|
userDetail: SandHubDetailDto,
|
|
ctx: MockInquiryCompanyContext,
|
|
): Record<string, unknown> {
|
|
const companyId = Number(ctx.companyId);
|
|
|
|
return {
|
|
data: {
|
|
id: 70016075946,
|
|
printNumber: "1405/1143-70591/220/1/0",
|
|
companyId: Number.isNaN(companyId) ? 15 : companyId,
|
|
companyName: ctx.companyName,
|
|
beginDate: "1405/01/17",
|
|
endDate: "1406/01/17",
|
|
issueDate: "1405/01/16",
|
|
hasEndorsement: null,
|
|
motorNumber: "TZ196XYAP223A210074",
|
|
chassisNumber: "LFP8C7PC3R1K12157",
|
|
vin: "LFP8C7PC3R1K12157",
|
|
plateTypeId: 9,
|
|
plateTypeTitle: "پلاک قدیمی",
|
|
platePartOne: 16,
|
|
plateSerialNumber: 60,
|
|
plateLetterid: 12,
|
|
plateLetterTitle: "م ",
|
|
vehicleSystemTitle: "فائو ( FAW )",
|
|
vehicleGroupId: 2,
|
|
vehicleGroupTitle: "سواری چهار سیلندر",
|
|
insurerNationalCode: userDetail.nationalCodeOfInsurer,
|
|
insurerName: "هانيه کارخانهء",
|
|
ownerNationalCode: userDetail.nationalCodeOfInsurer,
|
|
previousId: 70006331822,
|
|
noLossYearsCount: 4,
|
|
platePartThree: 498,
|
|
lossDocuments: [],
|
|
},
|
|
isSuccess: true,
|
|
statusCode: 200,
|
|
message: "عملیات با موفقیت انجام شد",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Bodies returned from `makeSandHubRequest` in mock mode (same shape callers expect from real API).
|
|
*/
|
|
private buildMockSandHubResponseForUrl(
|
|
url: string,
|
|
payload?: unknown,
|
|
plateMock?: Record<string, unknown>,
|
|
): any {
|
|
const u = (url || "").toLowerCase();
|
|
if (u.includes("personal-inquiry")) {
|
|
const p = payload as { nationalCode?: string } | undefined;
|
|
const nin = typeof p?.nationalCode === "string" ? p.nationalCode : "-";
|
|
return {
|
|
message: "success",
|
|
data: {
|
|
firstName: "نام",
|
|
lastName: "خانوادگی",
|
|
fatherName: "-",
|
|
birthCertificateNumber: "-",
|
|
nin,
|
|
},
|
|
};
|
|
}
|
|
if (u.includes("driver-license-check")) {
|
|
return {
|
|
data: {
|
|
IsSucceed: true,
|
|
Message: "mock",
|
|
},
|
|
};
|
|
}
|
|
if (u.includes("ownership")) {
|
|
return {
|
|
data: {
|
|
IsSuccess: true,
|
|
},
|
|
};
|
|
}
|
|
if (u.includes("sheba")) {
|
|
return {
|
|
ReturnValue: true,
|
|
HasError: false,
|
|
Message: "mock-sheba-ok",
|
|
};
|
|
}
|
|
if (u.includes("block-inquiry") || u.includes("tejarat")) {
|
|
return this.mapNewApiResponseToOldFormat(
|
|
plateMock ?? this.buildMockPlateInquiryRaw({
|
|
companyId: process.env.CLIENT_ID ?? "15",
|
|
companyName: process.env.CLIENT_NAME ?? "بیمه سامان",
|
|
}),
|
|
);
|
|
}
|
|
this.logger.warn(
|
|
`[MOCK] Unrecognized SandHub URL; returning generic OK: ${url}`,
|
|
);
|
|
return { data: {}, message: "mock-ok" };
|
|
}
|
|
|
|
private async getAccessToken(): Promise<string> {
|
|
if (this.loginToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
|
return this.loginToken;
|
|
}
|
|
|
|
this.logger.log(
|
|
"SandHub token is missing or expired. Fetching a new one...",
|
|
);
|
|
|
|
try {
|
|
const response = await firstValueFrom(
|
|
this.httpService.post(process.env.SANHUB_URL_LOGIN, {
|
|
email: process.env.SANHUB_USERNAME,
|
|
password: process.env.SANHUB_PASSWORD,
|
|
}),
|
|
);
|
|
|
|
const token = response.data.accessToken;
|
|
|
|
if (!token)
|
|
throw new Error("No access token returned from SandHub login");
|
|
|
|
this.loginToken = token;
|
|
this.tokenExpiry = new Date(Date.now() + 55 * 60 * 1000); // 55-minute expiry
|
|
|
|
this.logger.log("Successfully obtained a new SandHub token.");
|
|
|
|
return this.loginToken;
|
|
} catch (er) {
|
|
this.logger.error("Failed to login to SandHub:", er.message);
|
|
this.loginToken = null;
|
|
this.tokenExpiry = null;
|
|
throw new UnauthorizedException("SandHub authentication failed");
|
|
}
|
|
}
|
|
|
|
private async getTejaratAccessToken(): Promise<string> {
|
|
if (
|
|
this.tejaratAccessToken &&
|
|
this.tejaratTokenExpiry &&
|
|
this.tejaratTokenExpiry > new Date()
|
|
) {
|
|
return this.tejaratAccessToken;
|
|
}
|
|
|
|
const baseUrl =
|
|
process.env.TEJARAT_INQUIRY_BASE_URL ?? "http://82.99.202.245:3027";
|
|
const email = process.env.TEJARAT_INQUIRY_EMAIL;
|
|
const password = process.env.TEJARAT_INQUIRY_PASSWORD;
|
|
|
|
if (!email || !password) {
|
|
throw new UnauthorizedException(
|
|
"Tejarat inquiry credentials are not configured (TEJARAT_INQUIRY_EMAIL/TEJARAT_INQUIRY_PASSWORD)",
|
|
);
|
|
}
|
|
|
|
this.logger.log(
|
|
"Tejarat inquiry token is missing or expired. Fetching a new one...",
|
|
);
|
|
|
|
try {
|
|
const response = await firstValueFrom(
|
|
this.httpService.post(
|
|
`${baseUrl}/user/login`,
|
|
{ email, password },
|
|
{
|
|
headers: {
|
|
Accept: "*/*",
|
|
"Content-Type": "application/json",
|
|
},
|
|
timeout: 30000,
|
|
},
|
|
),
|
|
);
|
|
|
|
const token = response.data?.accessToken;
|
|
if (!token) {
|
|
throw new Error("No access token returned from Tejarat login");
|
|
}
|
|
|
|
this.tejaratAccessToken = token;
|
|
// No explicit expiry in response; keep a conservative cache window.
|
|
this.tejaratTokenExpiry = new Date(Date.now() + 55 * 60 * 1000);
|
|
|
|
return this.tejaratAccessToken;
|
|
} catch (er) {
|
|
this.logger.error(
|
|
"Failed to login to Tejarat inquiry:",
|
|
er?.message || er,
|
|
);
|
|
this.tejaratAccessToken = null;
|
|
this.tejaratTokenExpiry = null;
|
|
throw new UnauthorizedException("Tejarat inquiry authentication failed");
|
|
}
|
|
}
|
|
|
|
private async getEsgAccessToken(): Promise<string> {
|
|
if (this.esgAccessToken && this.esgTokenExpiry && this.esgTokenExpiry > new Date()) {
|
|
return this.esgAccessToken;
|
|
}
|
|
|
|
const baseUrl = process.env.ESG_URL;
|
|
const username = process.env.ESG_USERNAME;
|
|
const password = process.env.ESG_PASSWORD;
|
|
|
|
if (!baseUrl || !username || !password) {
|
|
throw new UnauthorizedException(
|
|
"ESG credentials are not configured (ESG_URL/ESG_USERNAME/ESG_PASSWORD)",
|
|
);
|
|
}
|
|
|
|
this.logger.log("ESG token is missing or expired. Fetching a new one...");
|
|
|
|
try {
|
|
const response = await firstValueFrom(
|
|
this.httpService.post(
|
|
`${baseUrl}/auth/login`,
|
|
{ username, password },
|
|
{
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
timeout: 30000,
|
|
},
|
|
),
|
|
);
|
|
|
|
const token = response.data?.accessToken;
|
|
if (!token) {
|
|
throw new Error("No access token returned from ESG login");
|
|
}
|
|
|
|
const expiresInSeconds = Number(response.data?.expiresIn ?? 900);
|
|
const ttlMs = Number.isFinite(expiresInSeconds)
|
|
? Math.max(30, expiresInSeconds - 60) * 1000
|
|
: 14 * 60 * 1000;
|
|
|
|
this.esgAccessToken = token;
|
|
this.esgTokenExpiry = new Date(Date.now() + ttlMs);
|
|
return this.esgAccessToken;
|
|
} catch (er: any) {
|
|
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");
|
|
}
|
|
}
|
|
|
|
private async makeEsgRequest(
|
|
url: string,
|
|
payload: any,
|
|
inquiryType: ExternalInquiryType,
|
|
options?: SandHubInquiryOptions,
|
|
maxRetries = 2,
|
|
) {
|
|
if (!(await this.isInquiryLive(inquiryType, options))) {
|
|
this.logger.log(`[MOCK] ESG POST skipped (${inquiryType}): ${url}`);
|
|
const ctx = await this.mockCompanyContext(options);
|
|
if (inquiryType === "thirdPartyPlate") {
|
|
return this.buildMockPlateInquiryRaw(ctx);
|
|
}
|
|
if (inquiryType === "personalIdentity") {
|
|
const nin =
|
|
typeof payload?.nationalCode === "string"
|
|
? payload.nationalCode
|
|
: "-";
|
|
return this.getDefaultMockPersonInquiry(nin);
|
|
}
|
|
if (inquiryType === "sheba") {
|
|
return {
|
|
ReturnValue: true,
|
|
HasError: false,
|
|
Message: "mock-sheba-ok",
|
|
};
|
|
}
|
|
return this.buildMockPlateInquiryRaw(ctx);
|
|
}
|
|
|
|
const INITIAL_DELAY = 500;
|
|
const BACKOFF_FACTOR = 2;
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
const token = await this.getEsgAccessToken();
|
|
try {
|
|
const response = await firstValueFrom(
|
|
this.httpService.post(url, payload, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
timeout: 30000,
|
|
}),
|
|
);
|
|
if (!response?.data) throw new Error("EMPTY_RESPONSE");
|
|
return response.data;
|
|
} catch (err: any) {
|
|
const status = err?.response?.status;
|
|
const data = err?.response?.data;
|
|
this.logger.error(
|
|
`ESG request failed (attempt ${attempt + 1}/${maxRetries}) to ${url} with status ${
|
|
status ?? "NO_STATUS"
|
|
}`,
|
|
data ? JSON.stringify(data) : err?.message || err,
|
|
);
|
|
|
|
if (status === 401) {
|
|
this.esgAccessToken = null;
|
|
this.esgTokenExpiry = null;
|
|
}
|
|
|
|
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 {
|
|
if (!raw) return raw;
|
|
|
|
if (raw?.success === false) {
|
|
return {
|
|
Error: {
|
|
Message:
|
|
raw?.error?.message ||
|
|
raw?.message ||
|
|
"ESG policyByPlate inquiry returned an error",
|
|
Code: raw?.error?.code || raw?.error?.providerCode || "ESG_INQUIRY_ERROR",
|
|
ProviderMessage: raw?.error?.providerMessage,
|
|
ProviderCode: raw?.error?.providerCode,
|
|
TrackingCode: raw?.trackingCode,
|
|
Conflict: raw?.error?.conflict,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Real ESG response wraps payload in { success: true, data: { ... } }.
|
|
// Mock data and Tejarat-format fallback are flat objects with no .data wrapper —
|
|
// in that case use raw itself so CompanyName / CompanyCode are preserved.
|
|
const data =
|
|
raw?.data != null && typeof raw?.data === "object" ? raw.data : raw;
|
|
return this.mapNewApiResponseToOldFormat({
|
|
...data,
|
|
companyId: data.CmpCod ?? data.companyId ?? data.CompanyCode,
|
|
companyPersianName:
|
|
data.CmpNam ?? data.companyPersianName ?? data.CompanyName,
|
|
carGrpCod: data.CarGrpCod ?? data.carGrpCod,
|
|
usgCod: data.UsgCod ?? data.usgCod,
|
|
vehSysCod: data.VehSysCod ?? data.vehSysCod,
|
|
mtrnum: data.MtrNum ?? data.mtrnum,
|
|
shsNam: data.ShsNum ?? data.ShsNam ?? data.shsNam,
|
|
vin: data.VIN ?? data.vin,
|
|
ntnlId: data.NtnlId ?? data.ntnlId,
|
|
fullname: data.InsNam ?? data.fullname,
|
|
ThirdPolicyCode: data.PlcyUnqCod ?? data.ThirdPolicyCode,
|
|
LastCompanyDocumentNumber:
|
|
data.LastCmpDocNo ?? data.LastCompanyDocumentNumber,
|
|
IssueDate: data.HIsuDte ?? data.IssueDate,
|
|
StartDate: data.HBgnDte ?? data.StartDate,
|
|
EndDate: data.HEndDte ?? data.EndDate,
|
|
EdrsJson: data.Edrses ?? data.EdrsJson,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Normalises Jalali birth dates to `YYYY-MM-DD` for ESG person inquiry.
|
|
* Unlike Tejarat/SandHub personal inquiry, ESG expects Jalali — not Gregorian.
|
|
*/
|
|
private normalizeJalaliBirthDateForEsg(
|
|
input: string | number | null | undefined,
|
|
): string | null {
|
|
if (input === null || input === undefined) return null;
|
|
|
|
const raw = typeof input === "number" ? String(input) : String(input).trim();
|
|
if (!raw) return null;
|
|
|
|
let year = 0;
|
|
let month = 0;
|
|
let day = 0;
|
|
|
|
const separated = raw.match(/^(\d{4})[\-/](\d{1,2})[\-/](\d{1,2})$/);
|
|
if (separated) {
|
|
year = parseInt(separated[1], 10);
|
|
month = parseInt(separated[2], 10);
|
|
day = parseInt(separated[3], 10);
|
|
} else {
|
|
const digits = raw.replace(/\D/g, "");
|
|
if (digits.length === 8) {
|
|
year = parseInt(digits.slice(0, 4), 10);
|
|
month = parseInt(digits.slice(4, 6), 10);
|
|
day = parseInt(digits.slice(6, 8), 10);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!year || !month || !day || year < 1300) return null;
|
|
|
|
const mm = String(month).padStart(2, "0");
|
|
const dd = String(day).padStart(2, "0");
|
|
return `${year}-${mm}-${dd}`;
|
|
}
|
|
|
|
private getDefaultMockPersonInquiry(nationalCode: string): Record<string, unknown> {
|
|
return {
|
|
firstName: "نام",
|
|
lastName: "خانوادگی",
|
|
fatherName: "-",
|
|
birthCertificateNumber: "-",
|
|
nin: nationalCode,
|
|
};
|
|
}
|
|
|
|
private mapEsgPersonInquiryToOldFormat(raw: any): Record<string, unknown> {
|
|
if (raw?.success === false) {
|
|
throw new NotFoundException(
|
|
raw?.error?.message ||
|
|
raw?.message ||
|
|
"Personal inquiry failed: Record not found for the given national code and birth date.",
|
|
);
|
|
}
|
|
|
|
const data = raw?.data ?? {};
|
|
return {
|
|
firstName: data.Name ?? data.firstName,
|
|
lastName: data.Family ?? data.lastName,
|
|
fatherName: data.FatherName ?? data.fatherName,
|
|
birthCertificateNumber:
|
|
data.Shenasnameserial ??
|
|
data.ShenasnameNo ??
|
|
data.birthCertificateNumber,
|
|
nin: data.Nin ?? data.nationalCode ?? data.nin,
|
|
fullName: data.fullName,
|
|
birthDate: data.BirthDate ?? data.birthDate,
|
|
gender: data.Gender,
|
|
trackingCode: raw?.trackingCode,
|
|
};
|
|
}
|
|
|
|
private mapEsgShebaInquiryToOldFormat(raw: any): Record<string, unknown> {
|
|
if (raw?.success === false) {
|
|
throw new BadRequestException(
|
|
raw?.error?.message ||
|
|
raw?.message ||
|
|
"Sheba ID validation failed. The provided Sheba ID does not match the national ID.",
|
|
);
|
|
}
|
|
|
|
const data = raw?.data ?? {};
|
|
return {
|
|
ReturnValue: data.ReturnValue ?? true,
|
|
HasError: data.HasError ?? false,
|
|
Errors: data.Errors ?? {},
|
|
Message: raw?.message ?? data.Message,
|
|
trackingCode: raw?.trackingCode,
|
|
};
|
|
}
|
|
|
|
private async makeTejaratRequest(
|
|
url: string,
|
|
payload: any,
|
|
inquiryType: ExternalInquiryType,
|
|
options?: SandHubInquiryOptions,
|
|
maxRetries = 2,
|
|
) {
|
|
if (!(await this.isInquiryLive(inquiryType, options))) {
|
|
this.logger.log(`[MOCK] Tejarat POST skipped (${inquiryType}): ${url}`);
|
|
const ctx = await this.mockCompanyContext(options);
|
|
return this.buildMockPlateInquiryRaw(ctx);
|
|
}
|
|
const INITIAL_DELAY = 500;
|
|
const BACKOFF_FACTOR = 2;
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
const token = await this.getTejaratAccessToken();
|
|
try {
|
|
const response = await firstValueFrom(
|
|
this.httpService.post(url, payload, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
timeout: 30000,
|
|
}),
|
|
);
|
|
if (!response?.data) throw new Error("EMPTY_RESPONSE");
|
|
return response.data;
|
|
} catch (err: any) {
|
|
const status = err?.response?.status;
|
|
const data = err?.response?.data;
|
|
|
|
this.logger.error(
|
|
`Tejarat request failed (attempt ${attempt + 1}/${maxRetries}) to ${url} with status ${
|
|
status ?? "NO_STATUS"
|
|
}`,
|
|
data ? JSON.stringify(data) : err?.message || err,
|
|
);
|
|
|
|
// If unauthorized, clear token and retry once
|
|
if (status === 401) {
|
|
this.tejaratAccessToken = null;
|
|
this.tejaratTokenExpiry = null;
|
|
}
|
|
|
|
const delay = INITIAL_DELAY * Math.pow(BACKOFF_FACTOR, attempt);
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
if (attempt === maxRetries - 1) {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tejarat block inquiry (replaces SandHub call for V2 flows).
|
|
* Returns both raw + mapped (old-format) response.
|
|
*/
|
|
async getTejaratBlockInquiry(
|
|
userDetail: SandHubDetailDto,
|
|
options?: SandHubInquiryOptions,
|
|
): Promise<{
|
|
raw: any;
|
|
mapped: any;
|
|
}> {
|
|
const ctx = await this.mockCompanyContext(options);
|
|
if (this.shouldUseEsgInquiryProvider()) {
|
|
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
|
const requestPayload = {
|
|
nationalCode: String(userDetail.nationalCodeOfInsurer),
|
|
plk1: String(userDetail.plate.leftDigits),
|
|
plk2: String(userDetail.plate.centerAlphabet),
|
|
plk3: String(userDetail.plate.centerDigits),
|
|
plksrl: String(userDetail.plate.ir),
|
|
};
|
|
|
|
const requestUrl = `${baseUrl}/inquiry/policyByPlate`;
|
|
const live = await this.isInquiryLive("thirdPartyPlate", options);
|
|
const raw = live
|
|
? await this.makeEsgRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"thirdPartyPlate",
|
|
options,
|
|
)
|
|
: this.buildMockPlateInquiryRaw(ctx);
|
|
if (!live) {
|
|
this.logger.debug(
|
|
`[MOCK] getEsgPolicyByPlateInquiry plate=${JSON.stringify(requestPayload)}`,
|
|
);
|
|
}
|
|
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw);
|
|
return { raw, mapped };
|
|
}
|
|
|
|
const baseUrl =
|
|
process.env.TEJARAT_INQUIRY_BASE_URL ?? "http://82.99.202.245:3027";
|
|
const requestPayload = {
|
|
leftTwoDigits: String(userDetail.plate.leftDigits),
|
|
serialLetter: String(userDetail.plate.centerAlphabet),
|
|
threeDigits: String(userDetail.plate.centerDigits),
|
|
rightTwoDigits: String(userDetail.plate.ir),
|
|
nationalCode: userDetail.nationalCodeOfInsurer,
|
|
};
|
|
|
|
const requestUrl = `${baseUrl}/block-inquiry-tejarat`;
|
|
const live = await this.isInquiryLive("thirdPartyPlate", options);
|
|
const raw = live
|
|
? await this.makeTejaratRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"thirdPartyPlate",
|
|
options,
|
|
)
|
|
: this.buildMockPlateInquiryRaw(ctx);
|
|
if (!live) {
|
|
this.logger.debug(
|
|
`[MOCK] getTejaratBlockInquiry plate=${JSON.stringify(requestPayload)}`,
|
|
);
|
|
}
|
|
const mapped = this.mapNewApiResponseToOldFormat(raw);
|
|
return { raw, mapped };
|
|
}
|
|
|
|
async getSandHubDataFromId(sandHubId) {
|
|
return await this.sandHubDbService.findOneBySandHubId(sandHubId);
|
|
}
|
|
|
|
async getTejaratCarBodyInquiry(
|
|
userDetail: SandHubDetailDto,
|
|
options?: SandHubInquiryOptions,
|
|
): Promise<{
|
|
raw: any;
|
|
mapped: Record<string, unknown>;
|
|
}> {
|
|
const baseUrl =
|
|
process.env.TEJARAT_INQUIRY_BASE_URL ?? "http://82.99.202.245:3027";
|
|
|
|
const requestPayload = {
|
|
part1: Number(userDetail.plate.leftDigits),
|
|
part2: String(userDetail.plate.centerAlphabet),
|
|
part3: Number(userDetail.plate.centerDigits),
|
|
part4: Number(userDetail.plate.ir),
|
|
nationalCode: String(userDetail.nationalCodeOfInsurer),
|
|
};
|
|
|
|
const requestUrl = `${baseUrl}/block-inquiry-tejarat/badane`;
|
|
const live = await this.isInquiryLive("carBodyPlate", options);
|
|
const ctx = await this.mockCompanyContext(options);
|
|
|
|
let raw: any;
|
|
|
|
if (live) {
|
|
const token = await this.getTejaratAccessToken();
|
|
try {
|
|
const response = await firstValueFrom(
|
|
this.httpService.post(requestUrl, requestPayload, {
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
timeout: 30000,
|
|
}),
|
|
);
|
|
raw = response.data;
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`[TEJARAT BADANE ERROR] Request failed for ${requestUrl}: ${error?.message || error}`,
|
|
error?.stack,
|
|
);
|
|
throw error;
|
|
}
|
|
} else {
|
|
this.logger.debug(
|
|
`[MOCK] getTejaratCarBodyInquiry plate=${JSON.stringify(requestPayload)}`,
|
|
);
|
|
raw = this.buildMockCarBodyInquiryRaw(userDetail, ctx);
|
|
}
|
|
|
|
const mapped = this.mapCarBodyInquiryResponse(raw);
|
|
return { raw, mapped };
|
|
}
|
|
|
|
/**
|
|
* Maps the Tejarat car-body (badane) API response to a consistent
|
|
* shape that mirrors the third-party inquiry mapped format.
|
|
* Callers should always use `mapped` for display; `raw` is kept for audit.
|
|
*/
|
|
private mapCarBodyInquiryResponse(raw: any): Record<string, unknown> {
|
|
if (!raw) return {};
|
|
|
|
const data = raw?.data ?? raw; // some responses nest under `data`
|
|
|
|
return {
|
|
// Identity
|
|
policyNumber: data.printNumber ?? data.insuranceNumber ?? null,
|
|
companyId: data.companyId ?? null,
|
|
CompanyCode: data.companyId ?? null,
|
|
CompanyName: data.companyName ?? data.companyPersianName ?? null,
|
|
|
|
// Insurer
|
|
insurerNationalCode:
|
|
data.insurerNationalCode ?? data.ownerNationalCode ?? null,
|
|
insurerName: data.insurerName ?? null,
|
|
ownerNationalCode: data.ownerNationalCode ?? null,
|
|
InsuranceFullName: data.insurerName ?? data.ownerName ?? null,
|
|
|
|
// Vehicle
|
|
motorNumber: data.motorNumber ?? data.EngineNumberField ?? null,
|
|
EngineNumberField: data.motorNumber ?? data.EngineNumberField ?? null,
|
|
chassisNumber: data.chassisNumber ?? data.ChassisNumberField ?? null,
|
|
ChassisNumberField: data.chassisNumber ?? data.ChassisNumberField ?? null,
|
|
vin: data.vin ?? data.VinNumberField ?? null,
|
|
VinNumberField: data.vin ?? data.VinNumberField ?? null,
|
|
vehicleGroupTitle: data.vehicleGroupTitle ?? null,
|
|
vehicleSystemTitle: data.vehicleSystemTitle ?? null,
|
|
MapTypNam: data.vehicleGroupTitle ?? data.vehicleSystemTitle ?? null,
|
|
|
|
// Policy dates (Jalali strings from the API)
|
|
IssueDate: data.issueDate ?? data.persianStartDate ?? null,
|
|
StartDate: data.beginDate ?? data.persianStartDate ?? null,
|
|
EndDate: data.endDate ?? data.persianEndDate ?? null,
|
|
|
|
// Plate
|
|
plateTypeTitle: data.plateTypeTitle ?? null,
|
|
platePartOne: data.platePartOne ?? null,
|
|
platePartThree: data.platePartThree ?? null,
|
|
plateSerialNumber: data.plateSerialNumber ?? null,
|
|
plateLetterTitle: data.plateLetterTitle ?? null,
|
|
|
|
// Loss / no-claim history
|
|
noLossYearsCount: data.noLossYearsCount ?? null,
|
|
lossDocuments: Array.isArray(data.lossDocuments)
|
|
? data.lossDocuments
|
|
: [],
|
|
|
|
// Misc
|
|
hasEndorsement: data.hasEndorsement ?? null,
|
|
previousId: data.previousId ?? null,
|
|
|
|
// Status passthrough
|
|
isSuccess: raw?.isSuccess ?? true,
|
|
statusCode: raw?.statusCode ?? 200,
|
|
message: raw?.message ?? null,
|
|
};
|
|
}
|
|
|
|
private async makeSandHubRequest(
|
|
url: string,
|
|
payload: any,
|
|
inquiryType: ExternalInquiryType,
|
|
options?: SandHubInquiryOptions,
|
|
maxRetries = 3,
|
|
) {
|
|
if (!(await this.isInquiryLive(inquiryType, options))) {
|
|
this.logger.log(`[MOCK] SandHub POST skipped (${inquiryType}): ${url}`);
|
|
const ctx = await this.mockCompanyContext(options);
|
|
const plateMock =
|
|
inquiryType === "thirdPartyPlate" || inquiryType === "carBodyPlate"
|
|
? this.buildMockPlateInquiryRaw(ctx)
|
|
: undefined;
|
|
return this.buildMockSandHubResponseForUrl(url, payload, plateMock);
|
|
}
|
|
const token = await this.getAccessToken();
|
|
const INITIAL_DELAY = 1000;
|
|
const BACKOFF_FACTOR = 2;
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
const response = await firstValueFrom(
|
|
this.httpService.post(url, payload, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
timeout: 50000,
|
|
}),
|
|
);
|
|
if (!response?.data) throw new Error("EMPTY_RESPONSE");
|
|
return response.data;
|
|
} catch (err) {
|
|
this.handleSandHubError(err, attempt, maxRetries);
|
|
const delay = INITIAL_DELAY * Math.pow(BACKOFF_FACTOR, attempt);
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
}
|
|
}
|
|
throw new BadGatewayException(
|
|
"Failed to fetch data from SandHub after multiple retries",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Maps the new SandHub API response format to the old format expected by the application
|
|
*/
|
|
private mapNewApiResponseToOldFormat(newResponse: any): any {
|
|
if (!newResponse) return newResponse;
|
|
|
|
// Map the new field names to the old field names
|
|
return {
|
|
...newResponse,
|
|
// Company information
|
|
CompanyCode: newResponse.companyId || newResponse.CompanyCode,
|
|
CompanyName: newResponse.companyPersianName || newResponse.CompanyName,
|
|
|
|
// Vehicle information
|
|
MapTypNam: newResponse.vehiclePersianName || newResponse.MapTypNam,
|
|
UsageField:
|
|
newResponse.persianCarType ||
|
|
newResponse.UsageField ||
|
|
(newResponse.usgCod === "8" ? "شخصی" : "نامشخص"),
|
|
MapUsageName: newResponse.MapUsageName || newResponse.MapUsageName,
|
|
|
|
// Financial coverage
|
|
FinancialCvrCptl:
|
|
newResponse.financeCoverage || newResponse.FinancialCvrCptl || "0",
|
|
|
|
// Dates
|
|
IssueDate: newResponse.persianStartDate || newResponse.IssueDate,
|
|
EndDate: newResponse.persianEndDate || newResponse.EndDate,
|
|
|
|
// Insurance details
|
|
LastCompanyDocumentNumber:
|
|
newResponse.lastCompanyInsuranceNumber ||
|
|
newResponse.LastCompanyDocumentNumber ||
|
|
newResponse.insuranceNumber,
|
|
|
|
// Technical details
|
|
MtrNum: newResponse.MtrNum || newResponse.mtrnum,
|
|
ShsNum:
|
|
newResponse.ShsNum ||
|
|
newResponse.shsNam ||
|
|
newResponse.ChassisNumberField,
|
|
vin: newResponse.vin || newResponse.VinNumberField || newResponse.vin,
|
|
VinNumberField: newResponse.vin || newResponse.VinNumberField,
|
|
ChassisNumberField: newResponse.ChassisNumberField || newResponse.shsNam,
|
|
EngineNumberField: newResponse.EngineNumberField || newResponse.mtrnum,
|
|
ModelField: newResponse.ModelField || newResponse.ModelField,
|
|
|
|
// Discount information
|
|
DisFnYrPrcnt: newResponse.DisFnYrPrcnt || newResponse.disFnYrPrcnt || "0",
|
|
DisLfYrPrcnt: newResponse.DisLfYrPrcnt || newResponse.disLfYrPrcnt || "0",
|
|
DisPrsnYrPrcnt:
|
|
newResponse.DisPrsnYrPrcnt || newResponse.disPrsnYrPrcnt || "0",
|
|
DisFnYrNum: newResponse.DisFnYrNum || newResponse.disFnYrNum,
|
|
DisLfYrNum: newResponse.DisLfYrNum || newResponse.disLfYrNum,
|
|
DisPrsnYrNum: newResponse.DisPrsnYrNum || newResponse.disPrsnYrNum,
|
|
|
|
// Usage and vehicle system codes
|
|
UsageCode: newResponse.UsageCode || newResponse.usgCod,
|
|
VehicleSystemCode: newResponse.VehicleSystemCode || newResponse.vehSysCod,
|
|
CarGroupCode: newResponse.CarGroupCode || newResponse.carGrpCod,
|
|
|
|
// Policy information
|
|
ThirdPolicyCode:
|
|
newResponse.ThirdPolicyCode || newResponse.ThirdPolicyCode,
|
|
|
|
// Endorsement data
|
|
EdrsJson:
|
|
newResponse.EdrsJson ||
|
|
(newResponse.edrSes ? JSON.stringify(newResponse.edrSes) : undefined),
|
|
|
|
// Insurance fullname
|
|
InsuranceFullName: newResponse.InsuranceFullName || newResponse.fullname,
|
|
|
|
// National code
|
|
nationalCode: newResponse.nationalCode || newResponse.ntnlId,
|
|
};
|
|
}
|
|
|
|
async getSandHubResponse(
|
|
userDetail: SandHubDetailDto,
|
|
options?: SandHubInquiryOptions,
|
|
) {
|
|
try {
|
|
const requestPayload = {
|
|
leftTwoDigits: String(userDetail.plate.leftDigits),
|
|
serialLetter: String(userDetail.plate.centerAlphabet),
|
|
threeDigits: String(userDetail.plate.centerDigits),
|
|
rightTwoDigits: String(userDetail.plate.ir),
|
|
nationalCode: userDetail.nationalCodeOfInsurer,
|
|
};
|
|
const base = process.env.SANHUB_BASE_URL ?? "";
|
|
const requestUrl = `${base}/block-inquiry-tejarat`;
|
|
|
|
let response: any;
|
|
if (await this.isInquiryLive("thirdPartyPlate", options)) {
|
|
response = await this.makeSandHubRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"thirdPartyPlate",
|
|
options,
|
|
);
|
|
} else {
|
|
this.logger.debug(
|
|
`[MOCK] getSandHubResponse plate=${JSON.stringify(requestPayload)}`,
|
|
);
|
|
const ctx = await this.mockCompanyContext(options);
|
|
response = this.buildMockPlateInquiryRaw(ctx);
|
|
}
|
|
|
|
const result = this.mapNewApiResponseToOldFormat(response);
|
|
|
|
// if (result.usgCod !== "8") {
|
|
// throw new Error("خودرو شما شخصی / سواری نمی باشد")
|
|
// }
|
|
|
|
return result;
|
|
} catch (err) {
|
|
throw new Error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Personal identity check.
|
|
* - CLIENT_ID=8 (Parsian/ESG): Jalali birth date sent as-is to `/inquiry/person`.
|
|
* - Other tenants: Tejarat/SandHub hub with Gregorian conversion.
|
|
*/
|
|
async getPersonalInquiry(
|
|
nationalCode: string,
|
|
birthDate: number | string,
|
|
options?: SandHubInquiryOptions,
|
|
) {
|
|
try {
|
|
if (this.shouldUseEsgInquiryProvider()) {
|
|
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").`,
|
|
);
|
|
}
|
|
|
|
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
|
const requestPayload = {
|
|
nationalCode: String(nationalCode),
|
|
birthDate: jalaliBirthDate,
|
|
dateHasPostfix: 0,
|
|
};
|
|
const requestUrl = `${baseUrl}/inquiry/person`;
|
|
const live = await this.isInquiryLive("personalIdentity", options);
|
|
|
|
if (!live) {
|
|
this.logger.debug(
|
|
`[MOCK] getEsgPersonInquiry nationalCode=${nationalCode} birthDate=${jalaliBirthDate}`,
|
|
);
|
|
return this.getDefaultMockPersonInquiry(String(nationalCode));
|
|
}
|
|
|
|
const raw = await this.makeEsgRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"personalIdentity",
|
|
options,
|
|
);
|
|
return this.mapEsgPersonInquiryToOldFormat(raw);
|
|
}
|
|
|
|
const requestUrl = `${process.env.SANHUB_BASE_URL}/personal-inquiry/tejarat-no`;
|
|
|
|
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").`,
|
|
);
|
|
}
|
|
|
|
const requestPayload = {
|
|
nationalCode,
|
|
birthdate: gregorianBirthdate,
|
|
};
|
|
|
|
const response = await this.makeSandHubRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"personalIdentity",
|
|
options,
|
|
);
|
|
|
|
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.",
|
|
);
|
|
}
|
|
return response.data;
|
|
} catch (err) {
|
|
if (
|
|
err instanceof BadRequestException ||
|
|
err instanceof NotFoundException
|
|
) {
|
|
throw err;
|
|
}
|
|
throw new Error(`Error in finding personal inquiry: ${err}`);
|
|
}
|
|
}
|
|
|
|
async getDrivingLicenseInfo(
|
|
nationalCode: string,
|
|
driverLicenseNumber: string,
|
|
options?: SandHubInquiryOptions,
|
|
) {
|
|
const requestUrl = `${process.env.SANHUB_BASE_URL}/driver-license-check`;
|
|
const requestPayload = {
|
|
driverLicenseNumber,
|
|
nationalCode,
|
|
};
|
|
|
|
this.logger.log(
|
|
`Fetching driving license info for national code: ${nationalCode}`,
|
|
);
|
|
|
|
try {
|
|
const response = await this.makeSandHubRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"drivingLicense",
|
|
options,
|
|
);
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
async getCarOwnershipInfo(
|
|
plate: any,
|
|
nationalCode: string,
|
|
options?: SandHubInquiryOptions,
|
|
) {
|
|
try {
|
|
const requestUrl = `${process.env.SANHUB_BASE_URL}/ownership`;
|
|
const requestPayload = {
|
|
Plk1: String(plate.leftDigits),
|
|
Plk2: String(plate.centerAlphabet),
|
|
Plk3: String(plate.centerDigits),
|
|
plkSrl: String(plate.ir),
|
|
nationalCode: nationalCode,
|
|
};
|
|
|
|
this.logger.log(
|
|
`Checking car ownership for national code: ${nationalCode}`,
|
|
);
|
|
const response = await this.makeSandHubRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"carOwnership",
|
|
options,
|
|
);
|
|
|
|
// Check the 'IsSuccess' field in the nested 'data' object.
|
|
if (response?.data?.IsSuccess === false) {
|
|
this.logger.warn(
|
|
`Car ownership check failed for national code ${nationalCode}. Response:`,
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
async getShebaValidation(
|
|
nationalId: string,
|
|
shebaId: string,
|
|
options?: SandHubInquiryOptions,
|
|
) {
|
|
try {
|
|
if (this.shouldUseEsgInquiryProvider()) {
|
|
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
|
const requestPayload = {
|
|
accountOwnerType: "1",
|
|
nationalCode: String(nationalId),
|
|
legalId: "",
|
|
sheba: String(shebaId),
|
|
};
|
|
const requestUrl = `${baseUrl}/inquiry/sheba`;
|
|
const live = await this.isInquiryLive("sheba", options);
|
|
|
|
this.logger.log(
|
|
`Validating Sheba ID via ESG for national code: ${nationalId}`,
|
|
);
|
|
|
|
if (!live) {
|
|
this.logger.debug(
|
|
`[MOCK] getEsgShebaInquiry nationalCode=${nationalId} sheba=${shebaId}`,
|
|
);
|
|
return {
|
|
ReturnValue: true,
|
|
HasError: false,
|
|
Message: "mock-sheba-ok",
|
|
};
|
|
}
|
|
|
|
const raw = await this.makeEsgRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"sheba",
|
|
options,
|
|
);
|
|
const response = this.mapEsgShebaInquiryToOldFormat(raw);
|
|
|
|
if (response?.ReturnValue === false || response?.HasError === true) {
|
|
this.logger.warn(
|
|
`Sheba validation failed for national code ${nationalId}. Response:`,
|
|
response,
|
|
);
|
|
throw new BadRequestException(
|
|
"Sheba ID validation failed. The provided Sheba ID does not match the national ID.",
|
|
);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
const requestUrl = `${process.env.SANHUB_BASE_URL}/sheba/sheba-tejaratno`;
|
|
const requestPayload = {
|
|
AccountOwnerType: "1",
|
|
NationalId: nationalId,
|
|
ShebaId: shebaId,
|
|
};
|
|
|
|
this.logger.log(`Validating Sheba ID for national code: ${nationalId}`);
|
|
const response = await this.makeSandHubRequest(
|
|
requestUrl,
|
|
requestPayload,
|
|
"sheba",
|
|
options,
|
|
);
|
|
|
|
if (response?.ReturnValue === false || response?.HasError === true) {
|
|
this.logger.warn(
|
|
`Sheba validation failed for national code ${nationalId}. Response:`,
|
|
response,
|
|
);
|
|
throw new BadRequestException(
|
|
"Sheba ID validation failed. The provided Sheba ID does not match the national ID.",
|
|
);
|
|
}
|
|
|
|
return response;
|
|
} catch (err) {
|
|
if (err instanceof BadRequestException) {
|
|
throw err;
|
|
}
|
|
throw new Error(`Error in matching sheba validation: ${err}`);
|
|
}
|
|
}
|
|
|
|
private handleSandHubError(
|
|
err: any,
|
|
attempt: number,
|
|
maxRetries: number,
|
|
): void {
|
|
if (err.response) {
|
|
this.logger.error(
|
|
`❌ SandHub responded with status ${err.response.status} on attempt ${attempt + 1}:`,
|
|
JSON.stringify(err.response.data),
|
|
);
|
|
|
|
if (err.response.status === 400) {
|
|
throw new BadGatewayException(
|
|
`SandHub rejected the request with a 400 Bad Request. Details: ${JSON.stringify(err.response.data)}`,
|
|
);
|
|
}
|
|
} else {
|
|
this.logger.error(
|
|
`❌ Network error during SandHub request (attempt ${attempt + 1}):`,
|
|
err.message,
|
|
);
|
|
}
|
|
|
|
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");
|
|
}
|
|
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) {
|
|
throw new BadGatewayException(
|
|
"Failed to fetch data from SandHub after multiple retries",
|
|
err.message,
|
|
);
|
|
}
|
|
}
|
|
|
|
async sandHubDocumentCreator(
|
|
userId: string,
|
|
requestId: string,
|
|
doc: Partial<SandHubModel>,
|
|
) {
|
|
const existingDoc = await this.sandHubDbService.findOneByRequestId(
|
|
requestId,
|
|
userId,
|
|
);
|
|
if (!existingDoc) {
|
|
return await this.sandHubDbService.create({ ...doc, userId, requestId });
|
|
} else {
|
|
const updatePayload = { ...existingDoc, ...doc };
|
|
return await this.sandHubDbService.findOneByRequestIdAndUpdate(
|
|
requestId,
|
|
userId,
|
|
updatePayload,
|
|
);
|
|
}
|
|
}
|
|
}
|