Files
CRM_Montzar/apps/api/src/integrations/tahesab/tahesab-http.client.ts
T
2026-09-05 11:35:20 +03:30

175 lines
5.7 KiB
TypeScript

import * as http from 'http';
import * as https from 'https';
import { URL } from 'url';
import {
BadGatewayException,
Injectable,
Logger,
RequestTimeoutException,
} from '@nestjs/common';
export type TahesabCallOptions = {
baseUrl: string;
token: string;
dbName: string;
/** مسیر OpenAPI مثل DoNewMoshtari یا CheckHealth-SSL */
path: string;
/** کلید body؛ اگر خالی باشد فقط GET بدون body */
bodyKey?: string;
args?: unknown[];
method?: 'GET' | 'POST';
allowInsecureTls?: boolean;
timeoutMs?: number;
/**
* اگر ته‌حساب با فیلد ERROR و متن منطبق جواب داد، به‌جای ۵۰۲
* مقدار خالی ([] ) برگردان — مثل «رکوردی یافت نشد» در DoListAsnad.
*/
emptyResultOnError?: RegExp;
};
function extractTahesabError(json: unknown, text: string): string {
if (json && typeof json === 'object') {
const o = json as Record<string, unknown>;
const err = o.ERROR ?? o.Error ?? o.error ?? o.message ?? o.Message;
if (typeof err === 'string' && err.trim()) return err.trim().slice(0, 300);
if (Array.isArray(err) && err.length) return String(err[0]).slice(0, 300);
}
const trimmed = text.trim();
return trimmed ? trimmed.slice(0, 300) : '';
}
@Injectable()
export class TahesabHttpClient {
private readonly logger = new Logger(TahesabHttpClient.name);
async call<T = unknown>(opts: TahesabCallOptions): Promise<T> {
const base = opts.baseUrl.replace(/\/+$/, '');
const path = opts.path.replace(/^\/+/, '');
const url = new URL(`${base}/${path}`);
const method = opts.method ?? (opts.bodyKey ? 'POST' : 'GET');
const timeoutMs = opts.timeoutMs ?? 30_000;
const headers: Record<string, string> = {
Authorization: `Bearer ${opts.token}`,
DBname: opts.dbName,
DBName: opts.dbName,
Accept: 'application/json',
Host: url.host,
};
let body: string | undefined;
if (method === 'POST' && opts.bodyKey) {
headers['Content-Type'] = 'application/json';
body = JSON.stringify({ [opts.bodyKey]: opts.args ?? [] });
headers['Content-Length'] = Buffer.byteLength(body).toString();
}
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const agent =
isHttps && opts.allowInsecureTls
? new https.Agent({ rejectUnauthorized: false })
: undefined;
return new Promise<T>((resolve, reject) => {
const req = lib.request(
{
protocol: url.protocol,
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: `${url.pathname}${url.search}`,
method,
headers,
timeout: timeoutMs,
agent,
},
(res) => {
const chunks: Buffer[] = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
const text = Buffer.concat(chunks).toString('utf8');
let json: unknown = null;
if (text) {
try {
json = JSON.parse(text);
} catch {
json = { raw: text };
}
}
const status = res.statusCode ?? 0;
const detail = extractTahesabError(json, text);
const isEmptyOk =
Boolean(detail) &&
Boolean(opts.emptyResultOnError) &&
opts.emptyResultOnError!.test(detail);
if (status < 200 || status >= 300) {
if (isEmptyOk) {
this.logger.debug(
`Tahesab ${path} ${status} empty-on-error: ${detail.slice(0, 120)}`,
);
resolve([] as T);
return;
}
this.logger.warn(
`Tahesab ${method} ${path} → ${status}: ${text.slice(0, 400)}`,
);
reject(
new BadGatewayException(
detail
? `ته‌حساب (${status}): ${detail}`
: `پاسخ نامعتبر ته‌حساب (${status}) برای ${path}`,
),
);
return;
}
// بعضی متدها با ۲۰۰ و فیلد ERROR برمی‌گردند
if (json && typeof json === 'object' && 'ERROR' in (json as object)) {
if (isEmptyOk) {
this.logger.debug(
`Tahesab ${path} empty-on-error: ${detail.slice(0, 120)}`,
);
resolve([] as T);
return;
}
reject(
new BadGatewayException(
detail ? `ته‌حساب: ${detail}` : `خطای ته‌حساب برای ${path}`,
),
);
return;
}
resolve(json as T);
});
},
);
req.on('timeout', () => {
req.destroy();
reject(
new RequestTimeoutException(
`مهلت اتصال به ته‌حساب برای ${path} به پایان رسید.`,
),
);
});
req.on('error', (err) => {
this.logger.error(`Tahesab call failed: ${path}`, err);
const raw = err.message || String(err);
const tlsHint =
/self-signed|certificate|UNABLE_TO_VERIFY|CERT_/i.test(raw)
? ' — در تنظیمات اتصال، گزینه «پذیرش گواهی SSL خودامضا» را فعال کنید.'
: '';
reject(
new BadGatewayException(
`اتصال به ته‌حساب ناموفق بود: ${raw}${tlsHint}`,
),
);
});
if (body) req.write(body);
req.end();
});
}
}