diff --git a/src/sand-hub/sand-hub.service.ts b/src/sand-hub/sand-hub.service.ts index 66da242..fee0119 100644 --- a/src/sand-hub/sand-hub.service.ts +++ b/src/sand-hub/sand-hub.service.ts @@ -26,6 +26,10 @@ export class SandHubService { 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, @@ -45,6 +49,10 @@ export class SandHubService { return process.env.CLIENT_ID ?? "15"; } + private shouldUseEsgInquiryProvider(): boolean { + return String(process.env.CLIENT_ID ?? "") === "8"; + } + private getMockInquiryCompanyName(): string { return process.env.CLIENT_NAME ?? "بیمه سامان"; } @@ -320,6 +328,244 @@ export class SandHubService { } } + private async getEsgAccessToken(): Promise { + 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 { + return { + firstName: "نام", + lastName: "خانوادگی", + fatherName: "-", + birthCertificateNumber: "-", + nin: nationalCode, + }; + } + + private mapEsgPersonInquiryToOldFormat(raw: any): Record { + 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 { + 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) { if (!(await this.useLiveSandHubApis())) { this.logger.log(`[MOCK] Tejarat POST skipped: ${url}`); @@ -377,6 +623,30 @@ export class SandHubService { raw: 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 = process.env.TEJARAT_INQUIRY_BASE_URL ?? "http://82.99.202.245:3027"; const requestPayload = { @@ -669,17 +939,43 @@ export class SandHubService { } /** - * Personal identity check against the Tejarat hub. The upstream gateway only - * accepts a Gregorian birthdate in `YYYY-MM-DD` form, but every caller in - * this codebase has the value as a Jalali date (number `13770624` or string - * `"1377-06-24"`/`"1377/06/24"`). We convert here so callers don't have to. + * 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) { 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 gregorianBirthdate = jalaliToGregorianDate(birthDate); - console.log(gregorianBirthdate); if (!gregorianBirthdate) { throw new BadRequestException( `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) { 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 requestPayload = { AccountOwnerType: "1", @@ -817,6 +1155,9 @@ export class SandHubService { return response; } catch (err) { + if (err instanceof BadRequestException) { + throw err; + } throw new Error(`Error in matching sheba validation: ${err}`); } }