1
0
forked from Yara724/api

Compare commits

..

2 Commits

Author SHA1 Message Date
SepehrYahyaee
a4eb98258b New inquiries for parsian 2026-06-17 14:32:25 +03:30
SepehrYahyaee
ad35d35065 Fixed repetetive emails closing mongo connection 2026-06-17 12:37:18 +03:30
2 changed files with 376 additions and 5 deletions

View File

@@ -4679,6 +4679,24 @@ export class RequestManagementService {
this.verifyExpertAccessForBlameV2(req, actor); this.verifyExpertAccessForBlameV2(req, actor);
const phone = (dto?.phoneNumber || "").trim(); const phone = (dto?.phoneNumber || "").trim();
if (!phone) throw new BadRequestException("phoneNumber is required"); if (!phone) throw new BadRequestException("phoneNumber is required");
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
const recentlySentToPhone = (req.history || [])
.slice()
.reverse()
.find(
(event: any) =>
event?.type === "PARTY_OTP_SENT" &&
event?.metadata?.phoneNumber === phone &&
event?.timestamp &&
new Date(event.timestamp) > twoMinutesAgo,
);
if (recentlySentToPhone) {
throw new BadRequestException(
`(${phone}): OTP was sent recently. Please wait 2 minutes before requesting again.`,
);
}
try { try {
await this.userAuthService.sendOtpRequest(phone); await this.userAuthService.sendOtpRequest(phone);
} catch (e: any) { } catch (e: any) {
@@ -4689,6 +4707,18 @@ export class RequestManagementService {
} }
throw e; throw e;
} }
if (!Array.isArray(req.history)) req.history = [];
req.history.push({
type: "PARTY_OTP_SENT",
actor: {
actorId: new Types.ObjectId(actor.sub),
actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(),
actorType:
actor?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert",
},
metadata: { phoneNumber: phone },
} as any);
await (req as any).save();
return { return {
sent: true, sent: true,
message: `OTP sent to ${phone}. Collect the code from the party, then call verify-party-otp.`, message: `OTP sent to ${phone}. Collect the code from the party, then call verify-party-otp.`,

View File

@@ -26,6 +26,10 @@ export class SandHubService {
private tejaratAccessToken: string | null = null; private tejaratAccessToken: string | null = null;
private tejaratTokenExpiry: Date | 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( constructor(
private readonly httpService: HttpService, private readonly httpService: HttpService,
private readonly sandHubDbService: SandHubDbService, private readonly sandHubDbService: SandHubDbService,
@@ -45,6 +49,10 @@ export class SandHubService {
return process.env.CLIENT_ID ?? "15"; return process.env.CLIENT_ID ?? "15";
} }
private shouldUseEsgInquiryProvider(): boolean {
return String(process.env.CLIENT_ID ?? "") === "8";
}
private getMockInquiryCompanyName(): string { private getMockInquiryCompanyName(): string {
return process.env.CLIENT_NAME ?? "بیمه سامان"; return process.env.CLIENT_NAME ?? "بیمه سامان";
} }
@@ -320,6 +328,244 @@ export class SandHubService {
} }
} }
private async getEsgAccessToken(): Promise<string> {
if (!(await this.useLiveSandHubApis())) {
return "mock-esg-access-token";
}
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, maxRetries = 2) {
if (!(await this.useLiveSandHubApis())) {
this.logger.log(`[MOCK] ESG POST skipped: ${url}`);
return this.getDefaultMockPlateInquiryRaw();
}
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,
},
};
}
const data = raw?.data ?? {};
return this.mapNewApiResponseToOldFormat({
...data,
companyId: data.CmpCod ?? data.companyId,
companyPersianName: data.CmpNam ?? data.companyPersianName,
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, maxRetries = 2) { private async makeTejaratRequest(url: string, payload: any, maxRetries = 2) {
if (!(await this.useLiveSandHubApis())) { if (!(await this.useLiveSandHubApis())) {
this.logger.log(`[MOCK] Tejarat POST skipped: ${url}`); this.logger.log(`[MOCK] Tejarat POST skipped: ${url}`);
@@ -377,6 +623,30 @@ export class SandHubService {
raw: any; raw: any;
mapped: any; mapped: any;
}> { }> {
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.useLiveSandHubApis();
const raw = live
? await this.makeEsgRequest(requestUrl, requestPayload)
: this.getDefaultMockPlateInquiryRaw();
if (!live) {
this.logger.debug(
`[MOCK] getEsgPolicyByPlateInquiry plate=${JSON.stringify(requestPayload)}`,
);
}
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw);
return { raw, mapped };
}
const baseUrl = const baseUrl =
process.env.TEJARAT_INQUIRY_BASE_URL ?? "http://82.99.202.245:3027"; process.env.TEJARAT_INQUIRY_BASE_URL ?? "http://82.99.202.245:3027";
const requestPayload = { const requestPayload = {
@@ -669,17 +939,43 @@ export class SandHubService {
} }
/** /**
* Personal identity check against the Tejarat hub. The upstream gateway only * Personal identity check.
* accepts a Gregorian birthdate in `YYYY-MM-DD` form, but every caller in * - CLIENT_ID=8 (Parsian/ESG): Jalali birth date sent as-is to `/inquiry/person`.
* this codebase has the value as a Jalali date (number `13770624` or string * - Other tenants: Tejarat/SandHub hub with Gregorian conversion.
* `"1377-06-24"`/`"1377/06/24"`). We convert here so callers don't have to.
*/ */
async getPersonalInquiry(nationalCode: string, birthDate: number | string) { async getPersonalInquiry(nationalCode: string, birthDate: number | string) {
try { 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.useLiveSandHubApis();
if (!live) {
this.logger.debug(
`[MOCK] getEsgPersonInquiry nationalCode=${nationalCode} birthDate=${jalaliBirthDate}`,
);
return this.getDefaultMockPersonInquiry(String(nationalCode));
}
const raw = await this.makeEsgRequest(requestUrl, requestPayload);
return this.mapEsgPersonInquiryToOldFormat(raw);
}
const requestUrl = `${process.env.SANHUB_BASE_URL}/personal-inquiry/tejarat-no`; const requestUrl = `${process.env.SANHUB_BASE_URL}/personal-inquiry/tejarat-no`;
const gregorianBirthdate = jalaliToGregorianDate(birthDate); const gregorianBirthdate = jalaliToGregorianDate(birthDate);
console.log(gregorianBirthdate);
if (!gregorianBirthdate) { if (!gregorianBirthdate) {
throw new BadRequestException( throw new BadRequestException(
`Invalid birth date for personal inquiry: ${birthDate}. Expected a Jalali date (e.g. 13770624 or "1377-06-24").`, `Invalid birth date for personal inquiry: ${birthDate}. Expected a Jalali date (e.g. 13770624 or "1377-06-24").`,
@@ -792,6 +1088,48 @@ export class SandHubService {
async getShebaValidation(nationalId: string, shebaId: string) { async getShebaValidation(nationalId: string, shebaId: string) {
try { 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.useLiveSandHubApis();
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);
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 requestUrl = `${process.env.SANHUB_BASE_URL}/sheba/sheba-tejaratno`;
const requestPayload = { const requestPayload = {
AccountOwnerType: "1", AccountOwnerType: "1",
@@ -817,6 +1155,9 @@ export class SandHubService {
return response; return response;
} catch (err) { } catch (err) {
if (err instanceof BadRequestException) {
throw err;
}
throw new Error(`Error in matching sheba validation: ${err}`); throw new Error(`Error in matching sheba validation: ${err}`);
} }
} }