forked from Shared/esg
moallem all endpoints working
This commit is contained in:
55
.local.env
Normal file
55
.local.env
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Application
|
||||||
|
PORT=8085
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
MONGODB_URI=mongodb://localhost:27017/inquiry-gateway
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
API_KEY=your-secure-api-key-here
|
||||||
|
JWT_SECRET=change-me-use-long-random-string
|
||||||
|
JWT_REFRESH_SECRET=change-me-refresh-secret
|
||||||
|
JWT_ACCESS_EXPIRES_IN=15m
|
||||||
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
|
JWT_ENABLED=true
|
||||||
|
BCRYPT_SALT_ROUNDS=12
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
THROTTLE_TTL=60
|
||||||
|
THROTTLE_LIMIT=100
|
||||||
|
|
||||||
|
# Provider routing (per inquiry type)
|
||||||
|
PERSON_DEFAULT_PROVIDER=TEJARATNOU
|
||||||
|
PERSON_FALLBACK_ENABLED=false
|
||||||
|
PERSON_FALLBACK_PROVIDERS=
|
||||||
|
CAR_PLATE_DEFAULT_PROVIDER=HAMTA
|
||||||
|
CAR_PLATE_FALLBACK_ENABLED=false
|
||||||
|
CAR_PLATE_FALLBACK_PROVIDERS=
|
||||||
|
|
||||||
|
# Hamta provider
|
||||||
|
HAMTA_BASE_URL=https://api.hamta.example.com
|
||||||
|
HAMTA_USERNAME=
|
||||||
|
HAMTA_PASSWORD=
|
||||||
|
HAMTA_SECRET_KEY=
|
||||||
|
HAMTA_TIMEOUT=10000
|
||||||
|
HAMTA_ENABLED=true
|
||||||
|
HAMTA_MAX_RETRIES=2
|
||||||
|
|
||||||
|
# Moallem provider (identical API structure to Hamta)
|
||||||
|
MOALLEM_BASE_URL=https://api.moallem.example.com
|
||||||
|
MOALLEM_USERNAME=
|
||||||
|
MOALLEM_PASSWORD=
|
||||||
|
MOALLEM_SECRET_KEY=
|
||||||
|
MOALLEM_TIMEOUT=10000
|
||||||
|
MOALLEM_ENABLED=true
|
||||||
|
MOALLEM_MAX_RETRIES=2
|
||||||
|
|
||||||
|
# TejaratNou provider
|
||||||
|
TEJARATNOU_BASE_URL=https://accounts.tejaratnoins.ir
|
||||||
|
TEJARATNOU_INQUIRY_BASE_URL=https://gateway.tejaratnoins.ir
|
||||||
|
TEJARATNOU_CLIENT_ID=api-gateway
|
||||||
|
TEJARATNOU_CLIENT_SECRET=hkld@ork123T
|
||||||
|
TEJARATNOU_USERNAME=thirdparty-silcogroup
|
||||||
|
TEJARATNOU_PASSWORD=FDHG87sdf787l764iuo
|
||||||
|
TEJARATNOU_TIMEOUT=15000
|
||||||
|
TEJARATNOU_ENABLED=true
|
||||||
48
package-lock.json
generated
48
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"socks-proxy-agent": "^7.0.0",
|
||||||
"uuid": "^11.0.3"
|
"uuid": "^11.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -7847,6 +7848,15 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://repo.hmirror.ir/artifactory/api/npm/mirror-npm/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://repo.hmirror.ir/artifactory/api/npm/mirror-npm/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://repo.hmirror.ir/artifactory/api/npm/mirror-npm/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -10927,6 +10937,44 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://repo.hmirror.ir/artifactory/api/npm/mirror-npm/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.9",
|
||||||
|
"resolved": "https://repo.hmirror.ir/artifactory/api/npm/mirror-npm/socks/-/socks-2.8.9.tgz",
|
||||||
|
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.1.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://repo.hmirror.ir/artifactory/api/npm/mirror-npm/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^6.0.2",
|
||||||
|
"debug": "^4.3.3",
|
||||||
|
"socks": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://repo.hmirror.ir/artifactory/api/npm/mirror-npm/source-map/-/source-map-0.7.4.tgz",
|
"resolved": "https://repo.hmirror.ir/artifactory/api/npm/mirror-npm/source-map/-/source-map-0.7.4.tgz",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"socks-proxy-agent": "^7.0.0",
|
||||||
"uuid": "^11.0.3"
|
"uuid": "^11.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
222
src/common/helpers/http-client.helper.ts
Normal file
222
src/common/helpers/http-client.helper.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import axios, {
|
||||||
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
CreateAxiosDefaults,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import type { Agent as HttpAgent } from 'http';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||||
|
|
||||||
|
const outboundHttpLogger = new Logger('OutboundHTTP');
|
||||||
|
|
||||||
|
let cachedSocksAgents: { httpAgent: HttpAgent; httpsAgent: HttpAgent } | undefined;
|
||||||
|
let outboundHttpDebugInstalled = false;
|
||||||
|
|
||||||
|
function getOutboundProxyUrl(): string | undefined {
|
||||||
|
const value = process.env.OUTBOUND_PROXY?.trim();
|
||||||
|
return value || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOutboundHttpDebugEnabled(): boolean {
|
||||||
|
return process.env.OUTBOUND_HTTP_DEBUG === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSocksProxy(proxyUrl: string): boolean {
|
||||||
|
return /^socks/i.test(proxyUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSocksAgents(proxyUrl: string): { httpAgent: HttpAgent; httpsAgent: HttpAgent } {
|
||||||
|
if (!cachedSocksAgents) {
|
||||||
|
const agent = new SocksProxyAgent(proxyUrl);
|
||||||
|
cachedSocksAgents = { httpAgent: agent, httpsAgent: agent };
|
||||||
|
}
|
||||||
|
return cachedSocksAgents;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncate(value: string, max = 800): string {
|
||||||
|
return value.length <= max ? value : `${value.slice(0, max)}...(truncated)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeForLog(data: unknown): string {
|
||||||
|
if (data === undefined || data === null) {
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return truncate(
|
||||||
|
data
|
||||||
|
.replace(/(<(?:\w+:)?Password[^>]*>)[^<]*(<\/(?:\w+:)?Password>)/gi, '$1***$2')
|
||||||
|
.replace(/password=[^&]*/gi, 'password=***')
|
||||||
|
.replace(/("password"\s*:\s*")[^"]*"/gi, '$1***"'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
|
||||||
|
return truncate(data.toString('utf8').replace(/password=[^&\r\n]*/gi, 'password=***'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(data, (key, value) => {
|
||||||
|
if (/password|secret|authorization|token|refresh/i.test(key)) {
|
||||||
|
return '***';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
return truncate(serialized);
|
||||||
|
} catch {
|
||||||
|
return truncate(String(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRequestUrl(config: InternalAxiosRequestConfig): string {
|
||||||
|
const url = config.url ?? '';
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = config.baseURL ?? '';
|
||||||
|
if (!base) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${base.replace(/\/$/, '')}/${url.replace(/^\//, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logOutboundRequest(config: InternalAxiosRequestConfig): void {
|
||||||
|
const method = (config.method ?? 'GET').toUpperCase();
|
||||||
|
const url = resolveRequestUrl(config);
|
||||||
|
outboundHttpLogger.log(`→ ${method} ${url}`);
|
||||||
|
|
||||||
|
if (config.params) {
|
||||||
|
outboundHttpLogger.log(` query=${sanitizeForLog(config.params)}`);
|
||||||
|
}
|
||||||
|
if (config.data !== undefined) {
|
||||||
|
outboundHttpLogger.log(` body=${sanitizeForLog(config.data)}`);
|
||||||
|
}
|
||||||
|
if (config.headers?.SOAPAction) {
|
||||||
|
outboundHttpLogger.log(` SOAPAction=${String(config.headers.SOAPAction)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logOutboundResponse(response: {
|
||||||
|
config: InternalAxiosRequestConfig;
|
||||||
|
status: number;
|
||||||
|
data: unknown;
|
||||||
|
}): void {
|
||||||
|
const start = (response.config as InternalAxiosRequestConfig & { _outboundStart?: number })
|
||||||
|
._outboundStart;
|
||||||
|
const durationMs = start ? Date.now() - start : undefined;
|
||||||
|
const method = (response.config.method ?? 'GET').toUpperCase();
|
||||||
|
const url = resolveRequestUrl(response.config);
|
||||||
|
|
||||||
|
outboundHttpLogger.log(
|
||||||
|
`← ${response.status} ${method} ${url}${durationMs !== undefined ? ` (${durationMs}ms)` : ''}`,
|
||||||
|
);
|
||||||
|
outboundHttpLogger.log(` response=${sanitizeForLog(response.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logOutboundError(error: AxiosError): void {
|
||||||
|
const config = error.config;
|
||||||
|
if (!config) {
|
||||||
|
outboundHttpLogger.error(`← FAILED | ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = (config as InternalAxiosRequestConfig & { _outboundStart?: number })._outboundStart;
|
||||||
|
const durationMs = start ? Date.now() - start : undefined;
|
||||||
|
const method = (config.method ?? 'GET').toUpperCase();
|
||||||
|
const url = resolveRequestUrl(config);
|
||||||
|
|
||||||
|
outboundHttpLogger.error(
|
||||||
|
`← FAILED ${method} ${url}${durationMs !== undefined ? ` (${durationMs}ms)` : ''} | status=${error.response?.status ?? 'N/A'} | ${error.message}`,
|
||||||
|
);
|
||||||
|
if (error.response?.data !== undefined) {
|
||||||
|
outboundHttpLogger.error(` response=${sanitizeForLog(error.response.data)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installOutboundHttpDebugInterceptors(): void {
|
||||||
|
if (!isOutboundHttpDebugEnabled() || outboundHttpDebugInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundHttpDebugInstalled = true;
|
||||||
|
outboundHttpLogger.log('Outbound HTTP debug logging enabled (OUTBOUND_HTTP_DEBUG=true)');
|
||||||
|
|
||||||
|
axios.interceptors.request.use((config) => {
|
||||||
|
(config as InternalAxiosRequestConfig & { _outboundStart?: number })._outboundStart =
|
||||||
|
Date.now();
|
||||||
|
logOutboundRequest(config);
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
logOutboundResponse(response);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error: AxiosError) => {
|
||||||
|
logOutboundError(error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
installOutboundHttpDebugInterceptors();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Axios defaults for outbound provider calls.
|
||||||
|
* Set OUTBOUND_PROXY to route traffic through an SSH tunnel (e.g. Termius SOCKS on 6565).
|
||||||
|
*/
|
||||||
|
export function getOutboundAxiosDefaults(): AxiosRequestConfig {
|
||||||
|
const proxyUrl = getOutboundProxyUrl();
|
||||||
|
if (!proxyUrl) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSocksProxy(proxyUrl)) {
|
||||||
|
const agents = getSocksAgents(proxyUrl);
|
||||||
|
return {
|
||||||
|
httpAgent: agents.httpAgent,
|
||||||
|
httpsAgent: agents.httpsAgent,
|
||||||
|
proxy: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = new URL(proxyUrl);
|
||||||
|
const defaultPort = parsed.protocol === 'https:' ? 443 : 80;
|
||||||
|
|
||||||
|
return {
|
||||||
|
proxy: {
|
||||||
|
protocol: parsed.protocol.replace(':', ''),
|
||||||
|
host: parsed.hostname,
|
||||||
|
port: Number(parsed.port || defaultPort),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeOutboundAxiosConfig(config: AxiosRequestConfig = {}): AxiosRequestConfig {
|
||||||
|
const outbound = getOutboundAxiosDefaults();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...outbound,
|
||||||
|
...config,
|
||||||
|
httpAgent: config.httpAgent ?? outbound.httpAgent,
|
||||||
|
httpsAgent: config.httpsAgent ?? outbound.httpsAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOutboundAxiosInstance(config: CreateAxiosDefaults = {}): AxiosInstance {
|
||||||
|
const outbound = getOutboundAxiosDefaults();
|
||||||
|
|
||||||
|
return axios.create({
|
||||||
|
...config,
|
||||||
|
httpAgent: config.httpAgent ?? outbound.httpAgent,
|
||||||
|
httpsAgent: config.httpsAgent ?? outbound.httpsAgent,
|
||||||
|
proxy: config.proxy ?? outbound.proxy,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { NormalizedErrorDto } from '../dto/normalized-error.dto';
|
||||||
|
|
||||||
export interface RequestLogContext {
|
export interface RequestLogContext {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
@@ -29,10 +30,12 @@ export class RequestLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logFailure(ctx: RequestLogContext, message: string, error?: unknown): void {
|
logFailure(ctx: RequestLogContext, message: string, error?: unknown): void {
|
||||||
this.logger.error(
|
const errorDetail = formatErrorDetail(error);
|
||||||
this.format({ ...ctx, success: false }, message),
|
const line = errorDetail
|
||||||
error instanceof Error ? error.stack : undefined,
|
? `${this.format({ ...ctx, success: false }, message)} | error=${errorDetail}`
|
||||||
);
|
: this.format({ ...ctx, success: false }, message);
|
||||||
|
|
||||||
|
this.logger.error(line, error instanceof Error ? error.stack : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private format(ctx: RequestLogContext, message: string): string {
|
private format(ctx: RequestLogContext, message: string): string {
|
||||||
@@ -49,3 +52,21 @@ export class RequestLogger {
|
|||||||
return parts.join(' | ');
|
return parts.join(' | ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatErrorDetail(error: unknown): string | undefined {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return error !== undefined ? String(error) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = (error as Error & { normalizedError?: NormalizedErrorDto }).normalizedError;
|
||||||
|
if (normalized?.providerMessage) {
|
||||||
|
const code = normalized.providerCode ? ` (${normalized.providerCode})` : '';
|
||||||
|
return `${normalized.providerMessage}${code}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized?.message && normalized.message !== error.message) {
|
||||||
|
return normalized.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|||||||
70
src/common/helpers/sayah-response.helper.ts
Normal file
70
src/common/helpers/sayah-response.helper.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export interface SayahApiResponse {
|
||||||
|
ReturnValue?: boolean;
|
||||||
|
HasError?: boolean;
|
||||||
|
IsSucceed?: boolean;
|
||||||
|
isSucceed?: boolean;
|
||||||
|
Errors?: Record<string, string> | Array<{ Code?: string; Message?: string }> | null;
|
||||||
|
errors?: Record<string, string> | Array<{ Code?: string; Message?: string }> | null;
|
||||||
|
Result?: {
|
||||||
|
ErrorMessage?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSayahErrors(
|
||||||
|
errors?: Record<string, string> | Array<{ Code?: string; Message?: string }> | null,
|
||||||
|
): string {
|
||||||
|
if (!errors) {
|
||||||
|
return 'Request failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(errors)) {
|
||||||
|
if (errors.length === 0) {
|
||||||
|
return 'Request failed';
|
||||||
|
}
|
||||||
|
return errors.map((error) => `${error.Code}: ${error.Message}`).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(errors);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return 'Request failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.map(([code, field]) => `${field} (code: ${code})`).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSayahProviderError(
|
||||||
|
body: SayahApiResponse,
|
||||||
|
): { message: string; code: string } | null {
|
||||||
|
if ('ReturnValue' in body || 'HasError' in body) {
|
||||||
|
if (body.HasError) {
|
||||||
|
return {
|
||||||
|
message: formatSayahErrors(body.Errors),
|
||||||
|
code: 'SAYAH_ERROR',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.ReturnValue === false) {
|
||||||
|
return {
|
||||||
|
message: formatSayahErrors(body.Errors),
|
||||||
|
code: 'SHEBA_MISMATCH',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSucceed = body.IsSucceed ?? body.isSucceed;
|
||||||
|
if (isSucceed === false) {
|
||||||
|
const errors = body.Errors ?? body.errors;
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
(Array.isArray(errors) || (errors && Object.keys(errors).length > 0)
|
||||||
|
? formatSayahErrors(errors)
|
||||||
|
: body.Result?.ErrorMessage) ?? 'Request failed',
|
||||||
|
code: 'API_ERROR',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
92
src/common/helpers/soap-civil-registration.helper.ts
Normal file
92
src/common/helpers/soap-civil-registration.helper.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
function decodeXml(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/&/g, '&');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSoapTextValue(value: string): string {
|
||||||
|
const nestedStrings = [
|
||||||
|
...value.matchAll(/<(?:[\w]+:)?string[^>]*>([\s\S]*?)<\/(?:[\w]+:)?string>/gi),
|
||||||
|
]
|
||||||
|
.map((match) => decodeXml(match[1].trim()))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (nestedStrings.length > 0) {
|
||||||
|
return nestedStrings.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeXml(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCiiEstelamResult(soapXml: string): Record<string, string> {
|
||||||
|
const civilBlockMatch =
|
||||||
|
soapXml.match(
|
||||||
|
/<(?:[\w]+:)?CiiEstelamResult[^>]*>([\s\S]*?)<\/(?:[\w]+:)?CiiEstelamResult>/i,
|
||||||
|
) ??
|
||||||
|
soapXml.match(
|
||||||
|
/<(?:[\w]+:)?SubmitInqDteStsWithPstCodResult[^>]*>([\s\S]*?)<\/(?:[\w]+:)?SubmitInqDteStsWithPstCodResult>/i,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!civilBlockMatch) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields: Record<string, string> = {};
|
||||||
|
const tagRegex = /<(?:[\w]+:)?(\w+)(?:[^>]*)>([\s\S]*?)<\/(?:[\w]+:)?\1>/gi;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = tagRegex.exec(civilBlockMatch[1])) !== null) {
|
||||||
|
fields[match[1]] = normalizeSoapTextValue(match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorNamsMatch = soapXml.match(
|
||||||
|
/<(?:[\w]+:)?ErrorNams(?![^>]*i:nil="true")[^>]*>([\s\S]*?)<\/(?:[\w]+:)?ErrorNams>/i,
|
||||||
|
);
|
||||||
|
if (errorNamsMatch) {
|
||||||
|
const errorNams = normalizeSoapTextValue(errorNamsMatch[1]);
|
||||||
|
if (errorNams) {
|
||||||
|
fields.ErrorNams = errorNams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCivilRegistrationProviderError(
|
||||||
|
fields: Record<string, string>,
|
||||||
|
): { message: string; code: string } | null {
|
||||||
|
const message = fields.Message?.trim();
|
||||||
|
const exceptionMessage = fields.ExceptionMessage?.trim();
|
||||||
|
const errorNams = fields.ErrorNams?.trim();
|
||||||
|
const nin = fields.Nin?.trim();
|
||||||
|
const hasIdentity = Boolean(fields.Name?.trim() || fields.Family?.trim());
|
||||||
|
|
||||||
|
if (exceptionMessage) {
|
||||||
|
return { message: exceptionMessage, code: 'PROVIDER_ERROR' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorNams) {
|
||||||
|
return { message: errorNams, code: 'PROVIDER_ERROR' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message && message.includes('err.')) {
|
||||||
|
return { message, code: 'RECORD_NOT_FOUND' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nin || nin === '0' || !hasIdentity) {
|
||||||
|
return {
|
||||||
|
message: message || 'Record not found',
|
||||||
|
code: 'RECORD_NOT_FOUND',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCivilRegistrationFullName(fields: Record<string, string>): string | undefined {
|
||||||
|
const fullName = [fields.Name, fields.Family].filter(Boolean).join(' ').trim();
|
||||||
|
return fullName || undefined;
|
||||||
|
}
|
||||||
@@ -7,6 +7,60 @@ export class PersonInquiryDataDto {
|
|||||||
@ApiProperty({ example: '1370-05-15' })
|
@ApiProperty({ example: '1370-05-15' })
|
||||||
birthDate!: string;
|
birthDate!: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 'علی محمدی' })
|
@ApiPropertyOptional({ example: 'سهیل حاجی زاده' })
|
||||||
fullName?: string;
|
fullName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '4311402422' })
|
||||||
|
Nin?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'سهيل' })
|
||||||
|
Name?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'حاجي زاده' })
|
||||||
|
Family?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'كوروش' })
|
||||||
|
FatherName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'د41' })
|
||||||
|
Shenasnameseri?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '709633' })
|
||||||
|
Shenasnameserial?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '0' })
|
||||||
|
ShenasnameNo?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '13781124' })
|
||||||
|
BirthDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '1' })
|
||||||
|
Gender?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '0' })
|
||||||
|
OfficeCode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '0' })
|
||||||
|
BookNo?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '0' })
|
||||||
|
BookRow?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '0' })
|
||||||
|
DeathStatus?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
Message?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
DeathDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
ExceptionMessage?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '1349689554' })
|
||||||
|
Zipcode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
ZipcodeDesc?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,76 +23,19 @@ export class InquiryService {
|
|||||||
private readonly inquiryLogService: InquiryLogService,
|
private readonly inquiryLogService: InquiryLogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Person inquiry — unified business flow with provider fallback and audit logging.
|
|
||||||
*/
|
|
||||||
async inquirePerson(
|
async inquirePerson(
|
||||||
dto: PersonInquiryRequestDto,
|
dto: PersonInquiryRequestDto,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
): Promise<BaseInquiryResponseDto<PersonInquiryDataDto>> {
|
): Promise<BaseInquiryResponseDto<PersonInquiryDataDto>> {
|
||||||
const trackingCode = generateTrackingCode();
|
return this.inquire(
|
||||||
const payload: PersonInquiryPayload = {
|
InquiryType.PERSON,
|
||||||
nationalCode: dto.nationalCode,
|
{
|
||||||
birthDate: dto.birthDate,
|
nationalCode: dto.nationalCode,
|
||||||
dateHasPostfix: (dto as PersonInquiryRequestDto & { dateHasPostfix?: number }).dateHasPostfix,
|
birthDate: dto.birthDate,
|
||||||
};
|
dateHasPostfix: dto.dateHasPostfix,
|
||||||
|
},
|
||||||
const start = Date.now();
|
requestId,
|
||||||
|
) as unknown as Promise<BaseInquiryResponseDto<PersonInquiryDataDto>>;
|
||||||
try {
|
|
||||||
const result = await this.executeInquiry<PersonInquiryPayload, PersonInquiryResult>(
|
|
||||||
InquiryType.PERSON,
|
|
||||||
payload,
|
|
||||||
requestId,
|
|
||||||
trackingCode,
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = this.buildSuccessResponse(
|
|
||||||
result.data,
|
|
||||||
result.provider,
|
|
||||||
trackingCode,
|
|
||||||
result.duration,
|
|
||||||
'Person inquiry completed successfully',
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.persistLog({
|
|
||||||
inquiryType: InquiryType.PERSON,
|
|
||||||
provider: result.provider,
|
|
||||||
requestPayload: payload as unknown as Record<string, unknown>,
|
|
||||||
responsePayload: response.data as unknown as Record<string, unknown>,
|
|
||||||
status: InquiryStatus.SUCCESS,
|
|
||||||
duration: result.duration,
|
|
||||||
requestId,
|
|
||||||
trackingCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - start;
|
|
||||||
const normalized = this.extractError(error);
|
|
||||||
|
|
||||||
const response: BaseInquiryResponseDto<PersonInquiryDataDto> = {
|
|
||||||
success: false,
|
|
||||||
provider: 'GATEWAY',
|
|
||||||
trackingCode,
|
|
||||||
message: normalized.message,
|
|
||||||
error: normalized,
|
|
||||||
duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.persistLog({
|
|
||||||
inquiryType: InquiryType.PERSON,
|
|
||||||
provider: ProviderName.HAMTA,
|
|
||||||
requestPayload: payload as unknown as Record<string, unknown>,
|
|
||||||
status: InquiryStatus.FAILURE,
|
|
||||||
duration,
|
|
||||||
requestId,
|
|
||||||
trackingCode,
|
|
||||||
error: normalized as unknown as Record<string, unknown>,
|
|
||||||
}).catch(() => undefined);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async inquire(
|
async inquire(
|
||||||
@@ -111,7 +54,7 @@ export class InquiryService {
|
|||||||
trackingCode,
|
trackingCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = this.buildGenericSuccessResponse(
|
const response = this.buildSuccessResponse(
|
||||||
result.data,
|
result.data,
|
||||||
result.provider,
|
result.provider,
|
||||||
trackingCode,
|
trackingCode,
|
||||||
@@ -172,27 +115,6 @@ export class InquiryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildSuccessResponse(
|
private buildSuccessResponse(
|
||||||
result: PersonInquiryResult,
|
|
||||||
provider: string,
|
|
||||||
trackingCode: string,
|
|
||||||
duration: number,
|
|
||||||
message: string,
|
|
||||||
): BaseInquiryResponseDto<PersonInquiryDataDto> {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
provider,
|
|
||||||
trackingCode,
|
|
||||||
message,
|
|
||||||
duration,
|
|
||||||
data: {
|
|
||||||
nationalCode: result.nationalCode,
|
|
||||||
birthDate: result.birthDate,
|
|
||||||
fullName: result.fullName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildGenericSuccessResponse(
|
|
||||||
result: unknown,
|
result: unknown,
|
||||||
provider: string,
|
provider: string,
|
||||||
trackingCode: string,
|
trackingCode: string,
|
||||||
@@ -210,6 +132,20 @@ export class InquiryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private toResponseData(result: unknown): Record<string, unknown> {
|
private toResponseData(result: unknown): Record<string, unknown> {
|
||||||
|
if (this.isPersonInquiryResult(result)) {
|
||||||
|
const raw =
|
||||||
|
result.raw && typeof result.raw === 'object' && !Array.isArray(result.raw)
|
||||||
|
? (result.raw as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
nationalCode: result.nationalCode,
|
||||||
|
birthDate: result.birthDate,
|
||||||
|
...(result.fullName ? { fullName: result.fullName } : {}),
|
||||||
|
...raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (result && typeof result === 'object' && 'raw' in result) {
|
if (result && typeof result === 'object' && 'raw' in result) {
|
||||||
const raw = (result as { raw: unknown }).raw;
|
const raw = (result as { raw: unknown }).raw;
|
||||||
return raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : { raw };
|
return raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : { raw };
|
||||||
@@ -220,6 +156,15 @@ export class InquiryService {
|
|||||||
: { raw: result };
|
: { raw: result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isPersonInquiryResult(result: unknown): result is PersonInquiryResult {
|
||||||
|
return (
|
||||||
|
typeof result === 'object' &&
|
||||||
|
result !== null &&
|
||||||
|
'nationalCode' in result &&
|
||||||
|
'birthDate' in result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private getInquiryLabel(inquiryType: InquiryType): string {
|
private getInquiryLabel(inquiryType: InquiryType): string {
|
||||||
return inquiryType.replace(/_INQUIRY$/, '').toLowerCase().replace(/_/g, ' ');
|
return inquiryType.replace(/_INQUIRY$/, '').toLowerCase().replace(/_/g, ' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||||
import { NormalizedErrorDto } from '../../common/dto/normalized-error.dto';
|
import { NormalizedErrorDto } from '../../common/dto/normalized-error.dto';
|
||||||
@@ -131,19 +132,41 @@ export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
|
|||||||
protected formatProviderError(
|
protected formatProviderError(
|
||||||
providerMessage?: string,
|
providerMessage?: string,
|
||||||
providerCode?: string,
|
providerCode?: string,
|
||||||
fallbackMessage = 'Provider returned an error',
|
fallbackMessage = 'Provider request failed',
|
||||||
): Error {
|
): Error {
|
||||||
|
const message = providerMessage?.trim() || fallbackMessage;
|
||||||
|
const code = providerCode?.trim() || 'PROVIDER_ERROR';
|
||||||
const normalized = this.normalizeError({
|
const normalized = this.normalizeError({
|
||||||
code: 'PROVIDER_ERROR',
|
code,
|
||||||
message: fallbackMessage,
|
message,
|
||||||
providerMessage,
|
providerMessage: message,
|
||||||
providerCode,
|
providerCode: code,
|
||||||
});
|
});
|
||||||
const err = new Error(normalized.message);
|
const err = new Error(message);
|
||||||
(err as Error & { normalizedError: NormalizedErrorDto }).normalizedError = normalized;
|
(err as Error & { normalizedError: NormalizedErrorDto }).normalizedError = normalized;
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected formatAxiosError(error: AxiosError): Error {
|
||||||
|
const data = error.response?.data;
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
const faultMatch = data.match(
|
||||||
|
/<(?:\w+:)?faultstring[^>]*>([\s\S]*?)<\/(?:\w+:)?faultstring>/i,
|
||||||
|
);
|
||||||
|
if (faultMatch?.[1]) {
|
||||||
|
return this.formatProviderError(
|
||||||
|
faultMatch[1].trim().replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'),
|
||||||
|
String(error.response?.status ?? 'NETWORK_ERROR'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatProviderError(
|
||||||
|
error.message,
|
||||||
|
String(error.response?.status ?? 'NETWORK_ERROR'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract callProvider(
|
protected abstract callProvider(
|
||||||
inquiryType: InquiryType,
|
inquiryType: InquiryType,
|
||||||
payload: TRequest,
|
payload: TRequest,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import axios, { AxiosError, AxiosInstance } from 'axios';
|
import { AxiosError, AxiosInstance } from 'axios';
|
||||||
|
import {
|
||||||
|
createOutboundAxiosInstance,
|
||||||
|
isOutboundHttpDebugEnabled,
|
||||||
|
} from '../../common/helpers/http-client.helper';
|
||||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||||
import { AmitisAuthServiceConfig } from '../../config/configuration';
|
import { AmitisAuthServiceConfig } from '../../config/configuration';
|
||||||
@@ -22,6 +26,8 @@ interface CentInsurTokenResponse {
|
|||||||
ExpiresIn?: number | string;
|
ExpiresIn?: number | string;
|
||||||
expiresIn?: number | string;
|
expiresIn?: number | string;
|
||||||
expires_in?: number | string;
|
expires_in?: number | string;
|
||||||
|
IsSucceed?: boolean;
|
||||||
|
LoginStatus?: number;
|
||||||
data?: CentInsurTokenResponse;
|
data?: CentInsurTokenResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +55,7 @@ export class AmitisProvider {
|
|||||||
private readonly generalTokenService: GeneralTokenService,
|
private readonly generalTokenService: GeneralTokenService,
|
||||||
) {
|
) {
|
||||||
this.config = configService.get<AmitisAuthServiceConfig>('amitis')!;
|
this.config = configService.get<AmitisAuthServiceConfig>('amitis')!;
|
||||||
this.httpClient = axios.create({
|
this.httpClient = createOutboundAxiosInstance({
|
||||||
baseURL: this.config.baseUrl,
|
baseURL: this.config.baseUrl,
|
||||||
timeout: this.config.timeout,
|
timeout: this.config.timeout,
|
||||||
});
|
});
|
||||||
@@ -102,25 +108,36 @@ export class AmitisProvider {
|
|||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Use FormData for multipart/form-data format
|
const loginBody = new URLSearchParams({
|
||||||
const FormData = require('form-data');
|
username: username.toLowerCase(),
|
||||||
const formData = new FormData();
|
password,
|
||||||
formData.append('username', username);
|
});
|
||||||
formData.append('password', password);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const loginUrl = `${this.config.baseUrl}${this.config.loginPath}`;
|
||||||
|
this.logger.log(
|
||||||
|
`AMITIS login → POST ${loginUrl} | provider=${providerName} | inquiry=${inquiryType} | username=${username.toLowerCase()}`,
|
||||||
|
);
|
||||||
|
|
||||||
const response = await this.httpClient.post<CentInsurTokenResponse>(
|
const response = await this.httpClient.post<CentInsurTokenResponse>(
|
||||||
this.config.loginPath,
|
this.config.loginPath,
|
||||||
formData,
|
loginBody.toString(),
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
...formData.getHeaders(),
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isOutboundHttpDebugEnabled()) {
|
||||||
|
this.logger.log(
|
||||||
|
`AMITIS login ← ${response.status} | provider=${providerName} | inquiry=${inquiryType} | bodyKeys=${Object.keys(response.data ?? {}).join(',') || 'none'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const token = this.extractToken(response.data);
|
const token = this.extractToken(response.data);
|
||||||
|
|
||||||
await this.saveToken(token, providerName, inquiryType, username, 'login');
|
await this.saveToken(token, providerName, inquiryType, username, password, 'login');
|
||||||
return token.accessToken;
|
return token.accessToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw this.toAuthError(`AMITIS login failed for ${providerName}/${inquiryType}`, error);
|
throw this.toAuthError(`AMITIS login failed for ${providerName}/${inquiryType}`, error);
|
||||||
@@ -144,6 +161,7 @@ export class AmitisProvider {
|
|||||||
providerName,
|
providerName,
|
||||||
inquiryType,
|
inquiryType,
|
||||||
latestToken.username,
|
latestToken.username,
|
||||||
|
latestToken.clientSecret,
|
||||||
'refresh',
|
'refresh',
|
||||||
);
|
);
|
||||||
return token.accessToken;
|
return token.accessToken;
|
||||||
@@ -160,15 +178,16 @@ export class AmitisProvider {
|
|||||||
providerName: ProviderName,
|
providerName: ProviderName,
|
||||||
inquiryType: InquiryType,
|
inquiryType: InquiryType,
|
||||||
username: string,
|
username: string,
|
||||||
|
password: string,
|
||||||
scope: string,
|
scope: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.generalTokenService.create({
|
await this.generalTokenService.create({
|
||||||
serviceProvider: this.getTokenKey(providerName, inquiryType),
|
serviceProvider: this.getTokenKey(providerName, inquiryType),
|
||||||
tokenType: token.tokenType,
|
tokenType: token.tokenType,
|
||||||
url: this.config.baseUrl,
|
url: this.config.baseUrl,
|
||||||
clientId: '',
|
clientId: username.toLowerCase(),
|
||||||
clientSecret: '',
|
clientSecret: password,
|
||||||
username,
|
username: username.toLowerCase(),
|
||||||
scope,
|
scope,
|
||||||
accessToken: token.accessToken,
|
accessToken: token.accessToken,
|
||||||
refreshToken: token.refreshToken,
|
refreshToken: token.refreshToken,
|
||||||
@@ -182,11 +201,21 @@ export class AmitisProvider {
|
|||||||
fallbackRefreshToken?: string,
|
fallbackRefreshToken?: string,
|
||||||
): AmitisAuthToken {
|
): AmitisAuthToken {
|
||||||
const body = responseBody.data ?? responseBody;
|
const body = responseBody.data ?? responseBody;
|
||||||
|
|
||||||
|
if (body.IsSucceed === false) {
|
||||||
|
throw new Error(
|
||||||
|
`AMITIS login rejected (LoginStatus=${body.LoginStatus ?? 'unknown'})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken =
|
const accessToken =
|
||||||
body.Token ?? body.token ?? body.AccessToken ?? body.accessToken ?? body.access_token;
|
body.Token ?? body.token ?? body.AccessToken ?? body.accessToken ?? body.access_token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken || !String(accessToken).trim()) {
|
||||||
throw new Error('AMITIS auth response did not include an access token');
|
const bodyPreview = JSON.stringify(responseBody).slice(0, 500);
|
||||||
|
throw new Error(
|
||||||
|
`AMITIS auth response did not include an access token | body=${bodyPreview}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresIn = Number(body.ExpiresIn ?? body.expiresIn ?? body.expires_in ?? 20 * 60);
|
const expiresIn = Number(body.ExpiresIn ?? body.expiresIn ?? body.expires_in ?? 20 * 60);
|
||||||
@@ -229,7 +258,13 @@ export class AmitisProvider {
|
|||||||
private toAuthError(message: string, error: unknown): Error {
|
private toAuthError(message: string, error: unknown): Error {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
const status = error.response?.status ?? 'NETWORK_ERROR';
|
const status = error.response?.status ?? 'NETWORK_ERROR';
|
||||||
return new Error(`${message}: ${status} ${error.message}`);
|
const body =
|
||||||
|
error.response?.data !== undefined
|
||||||
|
? JSON.stringify(error.response.data).slice(0, 500)
|
||||||
|
: undefined;
|
||||||
|
return new Error(
|
||||||
|
body ? `${message}: ${status} ${error.message} | body=${body}` : `${message}: ${status} ${error.message}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
|
||||||
|
import {
|
||||||
|
getSayahProviderError,
|
||||||
|
SayahApiResponse,
|
||||||
|
} from '../../common/helpers/sayah-response.helper';
|
||||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||||
import { ProviderEnvConfig } from '../../config/configuration';
|
import { ProviderEnvConfig } from '../../config/configuration';
|
||||||
@@ -107,11 +112,11 @@ export class HamtaProvider extends LegacyApiProvider {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get<Record<string, unknown>>(
|
const response = await axios.get<Record<string, unknown>>(
|
||||||
`${inquiryConfig.url}/AddressByPostcode`,
|
`${inquiryConfig.url}/AddressByPostcode`,
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
params: { PostalCode: postalCode },
|
params: { PostalCode: postalCode },
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
timeout: this.hamtaConfig.timeout,
|
timeout: this.hamtaConfig.timeout,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { raw: response.data };
|
return { raw: response.data };
|
||||||
@@ -146,14 +151,14 @@ export class HamtaProvider extends LegacyApiProvider {
|
|||||||
inquiryConfig.username,
|
inquiryConfig.username,
|
||||||
inquiryConfig.password,
|
inquiryConfig.password,
|
||||||
),
|
),
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/xml; charset=utf-8',
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
|
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
|
||||||
},
|
},
|
||||||
timeout: this.hamtaConfig.timeout,
|
timeout: this.hamtaConfig.timeout,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = this.extractSoapValue(response.data, 'SubmitInqDteStsWithPstCodResult');
|
const result = this.extractSoapValue(response.data, 'SubmitInqDteStsWithPstCodResult');
|
||||||
@@ -195,14 +200,14 @@ export class HamtaProvider extends LegacyApiProvider {
|
|||||||
inquiryConfig.username,
|
inquiryConfig.username,
|
||||||
inquiryConfig.password,
|
inquiryConfig.password,
|
||||||
),
|
),
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/xml; charset=utf-8',
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
SOAPAction: '"http://tempuri.org/IShahkarInq/ShahkarInquery"',
|
SOAPAction: '"http://tempuri.org/IShahkarInq/ShahkarInquery"',
|
||||||
},
|
},
|
||||||
timeout: this.hamtaConfig.timeout,
|
timeout: this.hamtaConfig.timeout,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = this.extractSoapValue(response.data, 'ShahkarInqueryResult');
|
const result = this.extractSoapValue(response.data, 'ShahkarInqueryResult');
|
||||||
@@ -241,11 +246,7 @@ export class HamtaProvider extends LegacyApiProvider {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<{
|
const response = await axios.post<SayahApiResponse>(
|
||||||
IsSucceed?: boolean;
|
|
||||||
Errors?: Array<{ Code?: string; Message?: string }>;
|
|
||||||
Result?: unknown;
|
|
||||||
}>(
|
|
||||||
inquiryConfig.url,
|
inquiryConfig.url,
|
||||||
{
|
{
|
||||||
AccountOwnerType: accountOwnerType,
|
AccountOwnerType: accountOwnerType,
|
||||||
@@ -253,20 +254,18 @@ export class HamtaProvider extends LegacyApiProvider {
|
|||||||
LegalId: legalId,
|
LegalId: legalId,
|
||||||
ShebaId: shebaId,
|
ShebaId: shebaId,
|
||||||
},
|
},
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: this.hamtaConfig.timeout,
|
timeout: this.hamtaConfig.timeout,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.data.IsSucceed && response.data.Errors) {
|
const providerError = getSayahProviderError(response.data);
|
||||||
throw this.formatProviderError(
|
if (providerError) {
|
||||||
this.formatErrors(response.data.Errors),
|
throw this.formatProviderError(providerError.message, providerError.code);
|
||||||
'SAYAH_HAS_ERROR',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { raw: response.data };
|
return { raw: response.data };
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
|
||||||
|
import {
|
||||||
|
buildCivilRegistrationFullName,
|
||||||
|
getCivilRegistrationProviderError,
|
||||||
|
parseCiiEstelamResult,
|
||||||
|
} from '../../common/helpers/soap-civil-registration.helper';
|
||||||
|
import {
|
||||||
|
getSayahProviderError,
|
||||||
|
SayahApiResponse,
|
||||||
|
} from '../../common/helpers/sayah-response.helper';
|
||||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||||
import { ProviderEnvConfig } from '../../config/configuration';
|
import { ProviderEnvConfig } from '../../config/configuration';
|
||||||
@@ -10,6 +20,7 @@ import {
|
|||||||
LegacyApiProvider,
|
LegacyApiProvider,
|
||||||
LegacyInquiryPayload,
|
LegacyInquiryPayload,
|
||||||
LegacyInquiryResult,
|
LegacyInquiryResult,
|
||||||
|
PersonInquiryResult,
|
||||||
} from '../shared/legacy-api.provider.abstract';
|
} from '../shared/legacy-api.provider.abstract';
|
||||||
|
|
||||||
interface CivilRegistrationPayload extends LegacyInquiryPayload {
|
interface CivilRegistrationPayload extends LegacyInquiryPayload {
|
||||||
@@ -107,11 +118,11 @@ export class MoallemProvider extends LegacyApiProvider {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get<Record<string, unknown>>(
|
const response = await axios.get<Record<string, unknown>>(
|
||||||
`${inquiryConfig.url}/AddressByPostcode`,
|
`${inquiryConfig.url}/AddressByPostcode`,
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
params: { PostalCode: postalCode },
|
params: { PostalCode: postalCode },
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
timeout: this.moallemConfig.timeout,
|
timeout: this.moallemConfig.timeout,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { raw: response.data };
|
return { raw: response.data };
|
||||||
@@ -128,7 +139,7 @@ export class MoallemProvider extends LegacyApiProvider {
|
|||||||
|
|
||||||
private async inquireCivilRegistration(
|
private async inquireCivilRegistration(
|
||||||
payload: CivilRegistrationPayload,
|
payload: CivilRegistrationPayload,
|
||||||
): Promise<{ raw: unknown }> {
|
): Promise<PersonInquiryResult> {
|
||||||
const nationalCode = this.getRequiredString(
|
const nationalCode = this.getRequiredString(
|
||||||
payload.nationalCode ?? payload.NIN,
|
payload.nationalCode ?? payload.NIN,
|
||||||
'nationalCode',
|
'nationalCode',
|
||||||
@@ -141,37 +152,39 @@ export class MoallemProvider extends LegacyApiProvider {
|
|||||||
const response = await axios.post<string>(
|
const response = await axios.post<string>(
|
||||||
inquiryConfig.url,
|
inquiryConfig.url,
|
||||||
this.buildCivilRegistrationEnvelope(
|
this.buildCivilRegistrationEnvelope(
|
||||||
|
payload,
|
||||||
nationalCode,
|
nationalCode,
|
||||||
birthDate,
|
birthDate,
|
||||||
inquiryConfig.username,
|
inquiryConfig.username,
|
||||||
inquiryConfig.password,
|
inquiryConfig.password,
|
||||||
),
|
),
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/xml; charset=utf-8',
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
SOAPAction: '"http://tempuri.org/ISabtV3/SabtInquery"',
|
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
|
||||||
},
|
},
|
||||||
timeout: this.moallemConfig.timeout,
|
timeout: this.moallemConfig.timeout,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = this.extractSoapValue(response.data, 'SabtInqueryResult');
|
const civilRegistration = parseCiiEstelamResult(response.data);
|
||||||
|
const providerError = getCivilRegistrationProviderError(civilRegistration);
|
||||||
|
if (providerError) {
|
||||||
|
throw this.formatProviderError(providerError.message, providerError.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullName = buildCivilRegistrationFullName(civilRegistration);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
raw: {
|
nationalCode,
|
||||||
nationalCode,
|
birthDate,
|
||||||
birthDate,
|
fullName: fullName || undefined,
|
||||||
result,
|
raw: civilRegistration,
|
||||||
soap: response.data,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
throw this.formatProviderError(
|
throw this.formatAxiosError(error);
|
||||||
error.message,
|
|
||||||
String(error.response?.status ?? 'NETWORK_ERROR'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -195,14 +208,14 @@ export class MoallemProvider extends LegacyApiProvider {
|
|||||||
inquiryConfig.username,
|
inquiryConfig.username,
|
||||||
inquiryConfig.password,
|
inquiryConfig.password,
|
||||||
),
|
),
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/xml; charset=utf-8',
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
SOAPAction: '"http://tempuri.org/IShahkarInq/ShahkarInquery"',
|
SOAPAction: '"http://tempuri.org/IShahkarInq/ShahkarInquery"',
|
||||||
},
|
},
|
||||||
timeout: this.moallemConfig.timeout,
|
timeout: this.moallemConfig.timeout,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = this.extractSoapValue(response.data, 'ShahkarInqueryResult');
|
const result = this.extractSoapValue(response.data, 'ShahkarInqueryResult');
|
||||||
@@ -241,11 +254,7 @@ export class MoallemProvider extends LegacyApiProvider {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<{
|
const response = await axios.post<SayahApiResponse>(
|
||||||
IsSucceed?: boolean;
|
|
||||||
Errors?: Array<{ Code?: string; Message?: string }>;
|
|
||||||
Result?: unknown;
|
|
||||||
}>(
|
|
||||||
inquiryConfig.url,
|
inquiryConfig.url,
|
||||||
{
|
{
|
||||||
AccountOwnerType: accountOwnerType,
|
AccountOwnerType: accountOwnerType,
|
||||||
@@ -253,20 +262,18 @@ export class MoallemProvider extends LegacyApiProvider {
|
|||||||
LegalId: legalId,
|
LegalId: legalId,
|
||||||
ShebaId: shebaId,
|
ShebaId: shebaId,
|
||||||
},
|
},
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: this.moallemConfig.timeout,
|
timeout: this.moallemConfig.timeout,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.data.IsSucceed && response.data.Errors) {
|
const providerError = getSayahProviderError(response.data);
|
||||||
throw this.formatProviderError(
|
if (providerError) {
|
||||||
this.formatErrors(response.data.Errors),
|
throw this.formatProviderError(providerError.message, providerError.code);
|
||||||
'SAYAH_HAS_ERROR',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { raw: response.data };
|
return { raw: response.data };
|
||||||
@@ -282,22 +289,38 @@ export class MoallemProvider extends LegacyApiProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildCivilRegistrationEnvelope(
|
private buildCivilRegistrationEnvelope(
|
||||||
|
payload: CivilRegistrationPayload,
|
||||||
nationalCode: string,
|
nationalCode: string,
|
||||||
birthDate: string,
|
birthDate: string,
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
): string {
|
): string {
|
||||||
|
const birthDateCompact = birthDate.replace(/-/g, '');
|
||||||
|
const dateHasPostfix = payload.dateHasPostfix ?? 0;
|
||||||
|
|
||||||
return `<?xml version="1.0" encoding="utf-8"?>
|
return `<?xml version="1.0" encoding="utf-8"?>
|
||||||
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||||
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
<soap:Body>
|
<soap:Body>
|
||||||
<SabtInquery xmlns="http://tempuri.org/">
|
<SubmitInqDteStsWithPstCod xmlns="http://tempuri.org/">
|
||||||
<NIN>${this.escapeXml(nationalCode)}</NIN>
|
<req>
|
||||||
<BirthDate>${this.escapeXml(birthDate)}</BirthDate>
|
<Nin>${this.escapeXml(nationalCode)}</Nin>
|
||||||
|
<Shenasnameserial>0</Shenasnameserial>
|
||||||
|
<ShenasnameNo>0</ShenasnameNo>
|
||||||
|
<BirthDate>${this.escapeXml(birthDateCompact)}</BirthDate>
|
||||||
|
<DateHasPostfix>${dateHasPostfix}</DateHasPostfix>
|
||||||
|
<Gender>0</Gender>
|
||||||
|
<OfficeCode>0</OfficeCode>
|
||||||
|
<BookNo>0</BookNo>
|
||||||
|
<NameHasPrefix>0</NameHasPrefix>
|
||||||
|
<NameHasPostFix>0</NameHasPostFix>
|
||||||
|
<FamilyHasPrefix>0</FamilyHasPrefix>
|
||||||
|
<FamilyHasPostFix>0</FamilyHasPostFix>
|
||||||
|
</req>
|
||||||
<Username>${this.escapeXml(username)}</Username>
|
<Username>${this.escapeXml(username)}</Username>
|
||||||
<Password>${this.escapeXml(password)}</Password>
|
<Password>${this.escapeXml(password)}</Password>
|
||||||
</SabtInquery>
|
</SubmitInqDteStsWithPstCod>
|
||||||
</soap:Body>
|
</soap:Body>
|
||||||
</soap:Envelope>`;
|
</soap:Envelope>`;
|
||||||
}
|
}
|
||||||
@@ -341,10 +364,10 @@ export class MoallemProvider extends LegacyApiProvider {
|
|||||||
|
|
||||||
private escapeXml(value: string): string {
|
private escapeXml(value: string): string {
|
||||||
return value
|
return value
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
|
||||||
|
import {
|
||||||
|
getSayahProviderError,
|
||||||
|
SayahApiResponse,
|
||||||
|
} from '../../common/helpers/sayah-response.helper';
|
||||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||||
import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface';
|
import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface';
|
||||||
@@ -172,16 +177,19 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<ParsianShahkarResponse>(inquiryConfig.url, {
|
const response = await axios.get<ParsianShahkarResponse>(
|
||||||
params: {
|
inquiryConfig.url,
|
||||||
nationalCode,
|
mergeOutboundAxiosConfig({
|
||||||
mobileNumber,
|
params: {
|
||||||
},
|
nationalCode,
|
||||||
headers: {
|
mobileNumber,
|
||||||
'X-PACKAGE-API-KEY': inquiryConfig.apiKey,
|
},
|
||||||
},
|
headers: {
|
||||||
timeout: this.config.timeout,
|
'X-PACKAGE-API-KEY': inquiryConfig.apiKey,
|
||||||
});
|
},
|
||||||
|
timeout: this.config.timeout,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const body = response.data;
|
const body = response.data;
|
||||||
return {
|
return {
|
||||||
@@ -230,14 +238,14 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
|
|||||||
inquiryConfig.username,
|
inquiryConfig.username,
|
||||||
inquiryConfig.password,
|
inquiryConfig.password,
|
||||||
),
|
),
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/xml; charset=utf-8',
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
|
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
|
||||||
},
|
},
|
||||||
timeout: this.config.timeout,
|
timeout: this.config.timeout,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -348,14 +356,14 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
|
|||||||
inquiryConfig.password,
|
inquiryConfig.password,
|
||||||
passwordFieldName,
|
passwordFieldName,
|
||||||
),
|
),
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/xml; charset=utf-8',
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
SOAPAction: `"http://tempuri.org/ICarAllPlcysV4/${methodName}"`,
|
SOAPAction: `"http://tempuri.org/ICarAllPlcysV4/${methodName}"`,
|
||||||
},
|
},
|
||||||
timeout: this.config.timeout,
|
timeout: this.config.timeout,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -396,7 +404,7 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<ParsianSayahResponse>(
|
const response = await axios.post<SayahApiResponse>(
|
||||||
inquiryConfig.url,
|
inquiryConfig.url,
|
||||||
{
|
{
|
||||||
AccountOwnerType: accountOwnerType,
|
AccountOwnerType: accountOwnerType,
|
||||||
@@ -404,23 +412,18 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
|
|||||||
LegalId: legalId,
|
LegalId: legalId,
|
||||||
ShebaId: shebaId,
|
ShebaId: shebaId,
|
||||||
},
|
},
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: this.config.timeout,
|
timeout: this.config.timeout,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
const providerError = getSayahProviderError(response.data);
|
||||||
(response.data.IsSucceed === false || response.data.isSucceed === false) &&
|
if (providerError) {
|
||||||
this.hasErrors(response.data.Errors ?? response.data.errors)
|
throw this.formatProviderError(providerError.message, providerError.code);
|
||||||
) {
|
|
||||||
throw this.formatProviderError(
|
|
||||||
this.formatErrors(response.data.Errors ?? response.data.errors),
|
|
||||||
'SAYAH_HAS_ERROR',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { raw: response.data };
|
return { raw: response.data };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import axios, { AxiosError, AxiosInstance } from 'axios';
|
import axios, { AxiosError, AxiosInstance } from 'axios';
|
||||||
|
import { createOutboundAxiosInstance, mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
|
||||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||||
import { jalaliDateToGregorianDate } from '../../common/helpers/jalali-date.helper';
|
import { jalaliDateToGregorianDate } from '../../common/helpers/jalali-date.helper';
|
||||||
@@ -51,7 +52,7 @@ export class TejaratNouProvider implements InquiryProvider<PersonInquiryPayload,
|
|||||||
private readonly cachedInquiryResultService: CachedInquiryResultService,
|
private readonly cachedInquiryResultService: CachedInquiryResultService,
|
||||||
) {
|
) {
|
||||||
this.config = this.configService.get<TejaratNouConfig>('tejaratnou')!;
|
this.config = this.configService.get<TejaratNouConfig>('tejaratnou')!;
|
||||||
this.httpClient = axios.create({
|
this.httpClient = createOutboundAxiosInstance({
|
||||||
baseURL: this.config.inquiryBaseUrl,
|
baseURL: this.config.inquiryBaseUrl,
|
||||||
timeout: this.config.timeout,
|
timeout: this.config.timeout,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -227,13 +228,13 @@ export class TejaratNouProvider implements InquiryProvider<PersonInquiryPayload,
|
|||||||
password: this.config.password,
|
password: this.config.password,
|
||||||
scope: 'api-gateway access-management',
|
scope: 'api-gateway access-management',
|
||||||
}),
|
}),
|
||||||
{
|
mergeOutboundAxiosConfig({
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
Cookie: 'cookiesession1=678A8C5D2222753B93723D81AA53FF8C',
|
Cookie: 'cookiesession1=678A8C5D2222753B93723D81AA53FF8C',
|
||||||
},
|
},
|
||||||
timeout: this.config.timeout,
|
timeout: this.config.timeout,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const jsonData = response.data;
|
const jsonData = response.data;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import axios, { AxiosError, AxiosInstance } from 'axios';
|
import { AxiosError, AxiosInstance } from 'axios';
|
||||||
|
import { createOutboundAxiosInstance } from '../../common/helpers/http-client.helper';
|
||||||
|
import { getSayahProviderError } from '../../common/helpers/sayah-response.helper';
|
||||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||||
import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface';
|
import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface';
|
||||||
@@ -51,7 +53,7 @@ export abstract class LegacyApiProvider extends BaseProvider<
|
|||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
this.providerConfig = config;
|
this.providerConfig = config;
|
||||||
this.httpClient = axios.create({
|
this.httpClient = createOutboundAxiosInstance({
|
||||||
timeout: config.timeout,
|
timeout: config.timeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -131,15 +133,9 @@ export abstract class LegacyApiProvider extends BaseProvider<
|
|||||||
|
|
||||||
// Handle Sayah/Sheba API format (ReturnValue/HasError)
|
// Handle Sayah/Sheba API format (ReturnValue/HasError)
|
||||||
if ('ReturnValue' in body || 'HasError' in body) {
|
if ('ReturnValue' in body || 'HasError' in body) {
|
||||||
if (body.HasError) {
|
const providerError = getSayahProviderError(body);
|
||||||
// Format error message from Errors object
|
if (providerError) {
|
||||||
const errorMessages = body.Errors
|
throw this.formatProviderError(providerError.message, providerError.code);
|
||||||
? Object.entries(body.Errors).map(([code, field]) => `${field} (code: ${code})`).join(', ')
|
|
||||||
: 'Request failed';
|
|
||||||
throw this.formatProviderError(
|
|
||||||
errorMessages,
|
|
||||||
'SAYAH_ERROR',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return { raw: body };
|
return { raw: body };
|
||||||
}
|
}
|
||||||
@@ -168,14 +164,11 @@ export abstract class LegacyApiProvider extends BaseProvider<
|
|||||||
const data = error.response?.data as LegacyProviderApiResponse | undefined;
|
const data = error.response?.data as LegacyProviderApiResponse | undefined;
|
||||||
|
|
||||||
// Check for Sayah/Sheba format error
|
// Check for Sayah/Sheba format error
|
||||||
if (data && ('ReturnValue' in data || 'HasError' in data) && data.HasError) {
|
if (data && ('ReturnValue' in data || 'HasError' in data)) {
|
||||||
const errorMessages = data.Errors
|
const providerError = getSayahProviderError(data);
|
||||||
? Object.entries(data.Errors).map(([code, field]) => `${field} (code: ${code})`).join(', ')
|
if (providerError) {
|
||||||
: error.message;
|
throw this.formatProviderError(providerError.message, providerError.code);
|
||||||
throw this.formatProviderError(
|
}
|
||||||
errorMessages,
|
|
||||||
String(error.response?.status ?? 'SAYAH_ERROR'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for CentInsur format error
|
// Check for CentInsur format error
|
||||||
|
|||||||
@@ -55,27 +55,34 @@ export class ProviderOrchestratorService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const normalized = this.extractError(error);
|
const normalized = this.extractError(error);
|
||||||
errors.push(normalized);
|
errors.push(normalized);
|
||||||
|
const errorSummary = normalized.providerMessage ?? normalized.message;
|
||||||
const hasNextProvider = providers.indexOf(provider) < providers.length - 1;
|
const hasNextProvider = providers.indexOf(provider) < providers.length - 1;
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
hasNextProvider
|
hasNextProvider
|
||||||
? `Provider ${provider.name} failed for ${inquiryType}, trying next fallback`
|
? `Provider ${provider.name} failed for ${inquiryType}, trying next fallback | ${errorSummary}`
|
||||||
: `Provider ${provider.name} failed for ${inquiryType}, no fallback available`,
|
: `Provider ${provider.name} failed for ${inquiryType}, no fallback available | ${errorSummary}`,
|
||||||
);
|
);
|
||||||
this.logger.debug(
|
this.logger.warn(
|
||||||
`Attempt duration: ${Date.now() - attemptStart}ms | error: ${normalized.message}`,
|
`Attempt duration: ${Date.now() - attemptStart}ms | providerCode=${normalized.providerCode ?? normalized.code} | providerMessage=${normalized.providerMessage ?? normalized.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InquiryException('All providers failed', {
|
const lastError = errors[errors.length - 1]!;
|
||||||
code: providers.length === 1 ? 'PROVIDER_SERVICE_NOT_AVAILABLE' : 'ALL_PROVIDERS_FAILED',
|
|
||||||
message:
|
if (providers.length === 1) {
|
||||||
providers.length === 1
|
throw new InquiryException(lastError.message, lastError);
|
||||||
? `${providers[0].name} service is not available`
|
}
|
||||||
: errors.map((e) => e.message).join('; '),
|
|
||||||
providerMessage: errors[0]?.providerMessage,
|
throw new InquiryException(
|
||||||
providerCode: errors[0]?.providerCode,
|
errors.map((error) => error.message).join('; '),
|
||||||
});
|
{
|
||||||
|
code: 'ALL_PROVIDERS_FAILED',
|
||||||
|
message: errors.map((error) => error.message).join('; '),
|
||||||
|
providerMessage: lastError.providerMessage ?? lastError.message,
|
||||||
|
providerCode: lastError.providerCode ?? lastError.code,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractError(error: unknown): NormalizedErrorDto {
|
private extractError(error: unknown): NormalizedErrorDto {
|
||||||
|
|||||||
@@ -106,8 +106,15 @@ console.debug = (...args: unknown[]) => {
|
|||||||
originalConsole.debug(...args);
|
originalConsole.debug(...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function shutdownTelemetry(): Promise<void> {
|
export async function shutdownTelemetry(): Promise<void> {
|
||||||
return sdk.shutdown();
|
try {
|
||||||
|
await sdk.shutdown();
|
||||||
|
} catch (error) {
|
||||||
|
originalConsole.warn(
|
||||||
|
'OpenTelemetry shutdown failed:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function recordHttpRequestStart(attributes: Record<string, string>): void {
|
export function recordHttpRequestStart(attributes: Record<string, string>): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user