forked from Shared/esg
moallem all endpoints working
This commit is contained in:
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 { NormalizedErrorDto } from '../dto/normalized-error.dto';
|
||||
|
||||
export interface RequestLogContext {
|
||||
requestId: string;
|
||||
@@ -29,10 +30,12 @@ export class RequestLogger {
|
||||
}
|
||||
|
||||
logFailure(ctx: RequestLogContext, message: string, error?: unknown): void {
|
||||
this.logger.error(
|
||||
this.format({ ...ctx, success: false }, message),
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
const errorDetail = formatErrorDetail(error);
|
||||
const line = errorDetail
|
||||
? `${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 {
|
||||
@@ -49,3 +52,21 @@ export class RequestLogger {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user